Migration of user interface to Jetpack Compose
This commit is contained in:
@ -1,79 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
ndkVersion '27.2.12479018'
|
||||
defaultConfig {
|
||||
applicationId = 'com.tutpro.baresip'
|
||||
minSdkVersion 28
|
||||
targetSdkVersion 35
|
||||
versionCode = 416
|
||||
versionName = '63.3.0'
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cFlags '-DHAVE_INTTYPES_H -lstdc++'
|
||||
arguments '-DANDROID_STL=c++_shared'
|
||||
version '3.25.1'
|
||||
}
|
||||
}
|
||||
ndk {
|
||||
// noinspection ChromeOsAbiSupport
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a' //, 'x86_64'
|
||||
}
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||
'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
dataBinding true
|
||||
buildConfig true
|
||||
}
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'src/main/cpp/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
namespace 'com.tutpro.baresip'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.7"
|
||||
implementation "androidx.core:core-ktx:1.15.0"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
implementation "androidx.activity:activity-ktx:1.10.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.8.6"
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
}
|
||||
116
app/build.gradle.kts
Normal file
116
app/build.gradle.kts
Normal file
@ -0,0 +1,116 @@
|
||||
plugins {
|
||||
alias(libs.plugins.compose.compiler)
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 35
|
||||
ndkVersion = "27.2.12479018"
|
||||
defaultConfig {
|
||||
applicationId = "com.tutpro.baresip"
|
||||
minSdk = 28
|
||||
targetSdk = 35
|
||||
versionCode = 416
|
||||
versionName = "63.3.0"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cFlags += "-DHAVE_INTTYPES_H -lstdc++"
|
||||
arguments.addAll(listOf("-DANDROID_STL=c++_shared"))
|
||||
version = "3.25.1"
|
||||
}
|
||||
}
|
||||
ndk {
|
||||
// noinspection ChromeOsAbiSupport
|
||||
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86_64"))
|
||||
}
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.7"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
//noinspection DataBindingWithoutKapt
|
||||
dataBinding = true
|
||||
buildConfig = true
|
||||
compose = true
|
||||
}
|
||||
sourceSets {
|
||||
getByName("main").java.srcDirs("src/main/kotlin")
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
namespace = "com.tutpro.baresip"
|
||||
}
|
||||
|
||||
composeCompiler {
|
||||
reportsDestination = layout.buildDirectory.dir("compose_compiler")
|
||||
// stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.foundation.android)
|
||||
implementation(libs.androidx.tools.core)
|
||||
implementation(libs.androidx.runtime.livedata)
|
||||
implementation(libs.androidx.ui.test.android)
|
||||
implementation(libs.androidx.foundation.android)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
val composeBom = libs.androidx.compose.bom
|
||||
implementation(composeBom)
|
||||
androidTestImplementation(composeBom)
|
||||
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.foundation)
|
||||
implementation(libs.androidx.ui)
|
||||
|
||||
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.kotlin.stdlib.jdk8)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.cardview)
|
||||
implementation(libs.androidx.localbroadcastmanager)
|
||||
implementation(libs.androidx.swiperefreshlayout)
|
||||
implementation(libs.androidx.preference.ktx)
|
||||
implementation(libs.androidx.exifinterface)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.androidx.lifecycle.extensions)
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
implementation(libs.androidx.fragment.ktx)
|
||||
implementation(libs.androidx.media)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
implementation(libs.ui.tooling.preview)
|
||||
debugImplementation(libs.ui.tooling)
|
||||
|
||||
}
|
||||
@ -45,7 +45,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar"
|
||||
tools:remove="android:appComponentFactory" >
|
||||
|
||||
<activity
|
||||
@ -53,7 +53,6 @@
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme.Main"
|
||||
android:windowSoftInputMode="adjustResize" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@ -78,7 +77,7 @@
|
||||
<activity
|
||||
android:name=".AccountsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/accounts"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
@ -86,7 +85,7 @@
|
||||
<activity
|
||||
android:name=".AccountActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/account" >
|
||||
</activity>
|
||||
|
||||
@ -94,6 +93,7 @@
|
||||
android:name=".CodecsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/codecs"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".AccountActivity" >
|
||||
</activity>
|
||||
|
||||
@ -101,12 +101,14 @@
|
||||
android:name=".ContactsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/contacts"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ContactActivity"
|
||||
android:name=".BaresipContactActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/contact"
|
||||
android:parentActivityName=".ContactsActivity" >
|
||||
</activity>
|
||||
@ -115,6 +117,7 @@
|
||||
android:name=".AndroidContactActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/contact"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".ContactsActivity" >
|
||||
</activity>
|
||||
|
||||
@ -122,6 +125,7 @@
|
||||
android:name=".ConfigActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/configuration"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
@ -129,6 +133,7 @@
|
||||
android:name=".AudioActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/audio_settings"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".ConfigActivity" >
|
||||
</activity>
|
||||
|
||||
@ -136,6 +141,7 @@
|
||||
android:name=".CallsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/call_history"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
@ -143,19 +149,21 @@
|
||||
android:name=".CallDetailsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/call_details"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".CallsActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ChatsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/chats"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ChatActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/chat" >
|
||||
</activity>
|
||||
@ -164,6 +172,7 @@
|
||||
android:name=".AboutActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/about_title"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:parentActivityName=".MainActivity" >
|
||||
</activity>
|
||||
|
||||
|
||||
@ -1799,10 +1799,11 @@ JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_module_1unload(
|
||||
(*env)->ReleaseStringUTFChars(env, javaModule, native_module);
|
||||
}
|
||||
|
||||
AAudioStream *AAudio_stream = NULL;
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_tutpro_baresip_Api_create_1AAudio_1SessionId(JNIEnv *env, jobject obj) {
|
||||
Java_com_tutpro_baresip_Api_AAudio_1open_1stream(JNIEnv *env, jobject obj) {
|
||||
AAudioStreamBuilder *builder = NULL;
|
||||
AAudioStream *stream = NULL;
|
||||
jint sessionId = -1;
|
||||
|
||||
if (AAudio_createStreamBuilder(&builder) != AAUDIO_OK) {
|
||||
@ -1819,15 +1820,22 @@ Java_com_tutpro_baresip_Api_create_1AAudio_1SessionId(JNIEnv *env, jobject obj)
|
||||
AAudioStreamBuilder_setChannelCount(builder, 1);
|
||||
AAudioStreamBuilder_setSessionId(builder, AAUDIO_SESSION_ID_ALLOCATE);
|
||||
|
||||
if (AAudioStreamBuilder_openStream(builder, &stream) == AAUDIO_OK) {
|
||||
sessionId = AAudioStream_getSessionId(stream);
|
||||
struct timespec td = {
|
||||
.tv_nsec = 100*1000*1000 /* 100ms */
|
||||
};
|
||||
nanosleep(&td, NULL);
|
||||
AAudioStream_close(stream);
|
||||
if (AAudioStreamBuilder_openStream(builder, &AAudio_stream) == AAUDIO_OK)
|
||||
sessionId = AAudioStream_getSessionId(AAudio_stream);
|
||||
else {
|
||||
LOGE("Failed to open AAudio stream\n");
|
||||
AAudio_stream = NULL;
|
||||
}
|
||||
|
||||
AAudioStreamBuilder_delete(builder);
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_tutpro_baresip_Api_AAudio_1close_1stream(JNIEnv *env, jobject obj)
|
||||
{
|
||||
if (AAudio_stream != NULL) {
|
||||
AAudioStream_close(AAudio_stream);
|
||||
AAudio_stream = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,43 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import com.tutpro.baresip.databinding.ActivityAboutBinding
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.text.HtmlCompat
|
||||
|
||||
class AboutActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityAboutBinding
|
||||
class AboutActivity : ComponentActivity() {
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
@ -30,31 +48,78 @@ class AboutActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
if (Build.VERSION.SDK_INT >= 35)
|
||||
binding.aboutText.updatePadding(top = 172)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
if (!Utils.isDarkTheme(this))
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
|
||||
enableEdgeToEdge()
|
||||
|
||||
Utils.addActivity("about")
|
||||
|
||||
val text = String.format(getString(R.string.about_text),
|
||||
BuildConfig.VERSION_NAME)
|
||||
binding.aboutText.text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
binding.aboutText.movementMethod = LinkMovementMethod.getInstance()
|
||||
val aboutTitle = String.format(getString(R.string.about_title))
|
||||
val aboutText = String.format(getString(R.string.about_text),
|
||||
BuildConfig.VERSION_NAME)
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
AboutContent(aboutTitle, aboutText) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AboutContent(title: String, text: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Localized description",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
) { contentPadding ->
|
||||
Text(
|
||||
text = AnnotatedString.Companion.fromHtml(
|
||||
htmlString = text,
|
||||
linkStyles = TextLinkStyles(
|
||||
style = SpanStyle(color = LocalCustomColors.current.accent)
|
||||
)
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.padding(contentPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
when (item.itemId) {
|
||||
@ -67,9 +132,8 @@ class AboutActivity : AppCompatActivity() {
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("about")
|
||||
setResult(Activity.RESULT_CANCELED, Intent())
|
||||
setResult(RESULT_CANCELED, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import java.net.URLEncoder
|
||||
@ -8,10 +10,10 @@ import java.net.URLDecoder
|
||||
|
||||
class Account(val accp: Long) {
|
||||
|
||||
var nickName = ""
|
||||
val nickName: MutableState<String> = mutableStateOf("")
|
||||
var displayName = Api.account_display_name(accp)
|
||||
val aor = Api.account_aor(accp)
|
||||
var luri = Api.account_luri(accp)
|
||||
private var luri = Api.account_luri(accp)
|
||||
var authUser = Api.account_auth_user(accp)
|
||||
var authPass = Api.account_auth_pass(accp)
|
||||
var outbound = ArrayList<String>()
|
||||
@ -68,7 +70,7 @@ class Account(val accp: Long) {
|
||||
|
||||
val extra = Api.account_extra(accp)
|
||||
if (Utils.paramExists(extra, "nickname"))
|
||||
nickName = Utils.paramValue(extra,"nickname")
|
||||
nickName.value = Utils.paramValue(extra,"nickname")
|
||||
if (Utils.paramExists(extra, "regint"))
|
||||
configuredRegInt = Utils.paramValue(extra,"regint").toInt()
|
||||
callHistory = Utils.paramValue(extra,"call_history") == ""
|
||||
@ -151,8 +153,8 @@ class Account(val accp: Long) {
|
||||
|
||||
var extra = ""
|
||||
|
||||
if (nickName != "")
|
||||
extra += ";nickname=$nickName"
|
||||
if (nickName.value != "")
|
||||
extra += ";nickname=${nickName.value}"
|
||||
|
||||
if (!callHistory)
|
||||
extra += ";call_history=no"
|
||||
@ -207,6 +209,13 @@ class Account(val accp: Long) {
|
||||
return aor.split("@")[1]
|
||||
}
|
||||
|
||||
fun text(): String {
|
||||
return if (nickName.value != "")
|
||||
nickName.value
|
||||
else
|
||||
aor.split(":")[1].substringBefore(";")
|
||||
}
|
||||
|
||||
private fun removeAudioCodecsStartingWith(prefix: String) {
|
||||
val newCodecs = ArrayList<String>()
|
||||
for (acSpec in audioCodec)
|
||||
@ -229,14 +238,14 @@ class Account(val accp: Long) {
|
||||
|
||||
fun accounts(): ArrayList<Account> {
|
||||
val res = ArrayList<Account>()
|
||||
for (ua in BaresipService.uas) {
|
||||
for (ua in BaresipService.uas.value) {
|
||||
res.add(ua.account)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
fun ofAor(aor: String): Account? {
|
||||
for (ua in BaresipService.uas)
|
||||
for (ua in BaresipService.uas.value)
|
||||
if (ua.account.aor == aor) return ua.account
|
||||
return null
|
||||
}
|
||||
@ -263,8 +272,8 @@ class Account(val accp: Long) {
|
||||
}
|
||||
|
||||
fun uniqueNickName(nickName: String): Boolean {
|
||||
for (ua in BaresipService.uas)
|
||||
if (ua.account.nickName == nickName)
|
||||
for (ua in BaresipService.uas.value)
|
||||
if (ua.account.nickName.value == nickName)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,80 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
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 com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
import java.util.*
|
||||
|
||||
class AccountListAdapter(private val cxt: Context, private val rows: ArrayList<AccountRow>) :
|
||||
ArrayAdapter<AccountRow>(cxt, R.layout.account_row, rows) {
|
||||
|
||||
private val layoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private class ViewHolder(view: View?) {
|
||||
val aorView: TextView = view?.findViewById(R.id.aor)!!
|
||||
val actionView: ImageButton = view?.findViewById(R.id.action)!!
|
||||
}
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
|
||||
val viewHolder: ViewHolder
|
||||
val rowView: View
|
||||
val ua = BaresipService.uas[position]
|
||||
|
||||
if (view == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.account_row, parent, false)
|
||||
viewHolder = ViewHolder(rowView)
|
||||
rowView.tag = viewHolder
|
||||
} else {
|
||||
rowView = view
|
||||
viewHolder = rowView.tag as ViewHolder
|
||||
}
|
||||
|
||||
val row = rows[position]
|
||||
viewHolder.aorView.text = row.aor.split(":")[0]
|
||||
viewHolder.aorView.textSize = 20f
|
||||
viewHolder.aorView.setPadding(6, 6, 0, 6)
|
||||
viewHolder.actionView.setImageResource(row.action)
|
||||
|
||||
viewHolder.aorView.setOnClickListener {
|
||||
val i = Intent(cxt, AccountActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", BaresipService.uas[position].account.aor)
|
||||
i.putExtras(b)
|
||||
MainActivity.accountRequest!!.launch(i)
|
||||
}
|
||||
|
||||
viewHolder.actionView.setOnClickListener {
|
||||
with (MaterialAlertDialogBuilder(cxt, R.style.AlertDialogTheme)) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(String.format(cxt.getString(R.string.delete_account),
|
||||
viewHolder.aorView.text))
|
||||
setPositiveButton(cxt.getText(R.string.delete)) { dialog, _ ->
|
||||
Api.ua_destroy(ua.uap)
|
||||
CallHistoryNew.clear(ua.account.aor)
|
||||
Message.clear(ua.account.aor)
|
||||
ua.remove()
|
||||
AccountsActivity.generateAccounts()
|
||||
AccountsActivity.saveAccounts()
|
||||
this@AccountListAdapter.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNeutralButton(cxt.getText(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
return rowView
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
class AccountRow(val aor: String, val action: Int)
|
||||
@ -1,27 +1,84 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.os.SystemClock
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import com.tutpro.baresip.databinding.ActivityAccountsBinding
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.StringReader
|
||||
import java.net.URL
|
||||
import java.util.Locale
|
||||
|
||||
class AccountsActivity : AppCompatActivity() {
|
||||
class AccountsActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var binding: ActivityAccountsBinding
|
||||
private lateinit var alAdapter: AccountListAdapter
|
||||
internal lateinit var aor: String
|
||||
private lateinit var mediaEncMap: Map<String, String>
|
||||
private lateinit var mediaNatMap: Map<String, String>
|
||||
|
||||
private var lastClick: Long = 0
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
@ -32,106 +89,380 @@ class AccountsActivity : AppCompatActivity() {
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAccountsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
if (Build.VERSION.SDK_INT >= 35)
|
||||
binding.AccountsView.updatePadding(top = 172)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (!Utils.isDarkTheme(this))
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
|
||||
val title = String.format(getString(R.string.accounts))
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
Utils.addActivity("accounts,$aor")
|
||||
|
||||
val listView = binding.accounts
|
||||
generateAccounts()
|
||||
alAdapter = AccountListAdapter(this, accounts)
|
||||
listView.adapter = alAdapter
|
||||
mediaEncMap = mapOf("zrtp" to "ZRTP", "dtls_srtp" to "DTLS-SRTPF",
|
||||
"srtp-mand" to "SRTP-MAND", "srtp" to "SRTP", "" to "--")
|
||||
|
||||
val accountRequest =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
|
||||
mediaNatMap = mapOf("stun" to "STUN", "turn" to "TURN", "ice" to "ICE", "" to "--")
|
||||
|
||||
val addAccountButton = binding.addAccount
|
||||
val newAorView = binding.newAor
|
||||
addAccountButton.setOnClickListener {
|
||||
val aor = newAorView.text.toString().trim()
|
||||
if (!Utils.checkAor("sip:$aor")) {
|
||||
Log.d(TAG, "Invalid Address of Record $aor")
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.invalid_aor), aor))
|
||||
return@setOnClickListener
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
AccountsScreen(this, title) { goBack() }
|
||||
}
|
||||
}
|
||||
if (Account.ofAor("sip:$aor") != null) {
|
||||
Log.d(TAG, "Account $aor already exists")
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.account_exists), aor.split(":")[0]))
|
||||
return@setOnClickListener
|
||||
}
|
||||
val laddr = "sip:$aor"
|
||||
val ua = UserAgent.uaAlloc("<$laddr>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;mwi=no")
|
||||
if (ua == null) {
|
||||
Log.e(TAG, "Failed to allocate UA for $aor")
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
getString(R.string.account_allocation_failure))
|
||||
return@setOnClickListener
|
||||
}
|
||||
// Api.account_debug(ua.account.accp)
|
||||
Log.d(TAG, "Allocated UA ${ua.uap} for ${Api.account_luri(ua.account.accp)}")
|
||||
newAorView.setText("")
|
||||
newAorView.hint = getString(R.string.user_domain)
|
||||
newAorView.clearFocus()
|
||||
generateAccounts()
|
||||
alAdapter.notifyDataSetChanged()
|
||||
saveAccounts()
|
||||
val i = Intent(this, AccountActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", ua.account.aor)
|
||||
b.putBoolean("new", true)
|
||||
i.putExtras(b)
|
||||
accountRequest.launch(i)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
if (BaresipService.activities.indexOf("accounts,$aor") == -1)
|
||||
return true
|
||||
|
||||
when (item.itemId) {
|
||||
|
||||
R.id.help -> {
|
||||
Utils.alertView(this@AccountsActivity, getString(R.string.new_account),
|
||||
getString(R.string.accounts_help))
|
||||
return true
|
||||
}
|
||||
|
||||
android.R.id.home -> {
|
||||
goBack()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = { NewAccount(ctx) },
|
||||
content = { contentPadding ->
|
||||
AccountsContent(ctx, contentPadding)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.accounts_menu, menu)
|
||||
return true
|
||||
@Composable
|
||||
fun AccountsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(top = 8.dp),
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(BaresipService.uas.value, key = { it.account.aor }) { ua ->
|
||||
val account = ua.account
|
||||
val aor = account.aor
|
||||
val text = if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
account.aor.substringAfter(":")
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 20.sp,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier.weight(1f).padding(start = 10.dp)
|
||||
.clickable {
|
||||
val i = Intent(ctx, AccountActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
i.putExtras(b)
|
||||
startActivity(i)
|
||||
}
|
||||
)
|
||||
SmallFloatingActionButton(
|
||||
onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
with(
|
||||
MaterialAlertDialogBuilder(
|
||||
ctx,
|
||||
R.style.AlertDialogTheme
|
||||
)
|
||||
) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
ctx.getString(R.string.delete_account),
|
||||
text
|
||||
)
|
||||
)
|
||||
setPositiveButton(R.string.delete) { dialog, _ ->
|
||||
CallHistoryNew.clear(aor)
|
||||
Message.clearMessagesOfAor(aor)
|
||||
ua.remove()
|
||||
Api.ua_destroy(ua.uap)
|
||||
saveAccounts()
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNeutralButton(ctx.getText(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
contentColor = LocalCustomColors.current.secondary
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.delete)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewAccount(ctx: Context) {
|
||||
var newAor by remember { mutableStateOf("") }
|
||||
val focusManager = LocalFocusManager.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newAor,
|
||||
placeholder = { CustomElements.Text(text = getString(R.string.new_account)) },
|
||||
onValueChange = { newAor = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.clickable {
|
||||
Utils.alertView(ctx, getString(R.string.new_account),
|
||||
getString(R.string.accounts_help)) },
|
||||
singleLine = false,
|
||||
trailingIcon = {
|
||||
if (newAor.isNotEmpty()) {
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = "Clear",
|
||||
modifier = Modifier.clickable { newAor = "" }
|
||||
)
|
||||
}
|
||||
},
|
||||
label = {
|
||||
CustomElements.Text(
|
||||
text = getString(R.string.new_account),
|
||||
fontSize = 18.sp
|
||||
)
|
||||
},
|
||||
textStyle = TextStyle(fontSize = 18.sp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.plus),
|
||||
contentDescription = "Add",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(48.dp)
|
||||
.clickable(
|
||||
onClick = {
|
||||
val account = createNew(newAor.trim())
|
||||
if (account != null) {
|
||||
val i = Intent(ctx, AccountActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", account.aor)
|
||||
i.putExtras(b)
|
||||
startActivity(i)
|
||||
newAor = ""
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNew(newAor: String): Account? {
|
||||
|
||||
val aor = if (newAor.startsWith("sip:"))
|
||||
newAor
|
||||
else
|
||||
"sip:$newAor"
|
||||
|
||||
if (!Utils.checkAor(aor)) {
|
||||
Log.d(TAG, "Invalid Address of Record $aor")
|
||||
Utils.alertView(
|
||||
this, getString(R.string.notice),
|
||||
String.format(getString(R.string.invalid_aor),
|
||||
aor.split(":")[1])
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (Account.ofAor(aor) != null) {
|
||||
Log.d(TAG, "Account $aor already exists")
|
||||
Utils.alertView(
|
||||
this, getString(R.string.notice),
|
||||
String.format(getString(R.string.account_exists),
|
||||
aor.split(":")[1])
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
val ua = UserAgent.uaAlloc(
|
||||
"<$aor>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;mwi=no"
|
||||
)
|
||||
if (ua == null) {
|
||||
Log.e(TAG, "Failed to allocate UA for $aor")
|
||||
Utils.alertView(
|
||||
this, getString(R.string.notice),
|
||||
getString(R.string.account_allocation_failure)
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// Api.account_debug(ua.account.accp)
|
||||
val acc = ua.account
|
||||
Log.d(TAG, "Allocated UA ${ua.uap} for ${Api.account_luri(acc.accp)}")
|
||||
initAccountFromConfig(this@AccountsActivity, acc)
|
||||
saveAccounts()
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
private fun initAccountFromConfig(ctx: AccountsActivity, acc: Account) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val url = "https://${Utils.uriHostPart(acc.aor)}/baresip/account_config.xml"
|
||||
val config = try {
|
||||
URL(url).readText()
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.d(TAG, "Failed to get account configuration from network")
|
||||
null
|
||||
}
|
||||
if (config != null && !ctx.isFinishing) {
|
||||
Log.d(TAG, "Got account config '$config'")
|
||||
val parserFactory: XmlPullParserFactory = XmlPullParserFactory.newInstance()
|
||||
val parser: XmlPullParser = parserFactory.newPullParser()
|
||||
parser.setInput(StringReader(config))
|
||||
var tag: String?
|
||||
var text = ""
|
||||
var event = parser.eventType
|
||||
val audioCodecs = ArrayList(Api.audio_codecs().split(","))
|
||||
val videoCodecs = ArrayList(Api.video_codecs().split(","))
|
||||
while (event != XmlPullParser.END_DOCUMENT) {
|
||||
tag = parser.name
|
||||
when (event) {
|
||||
XmlPullParser.TEXT ->
|
||||
text = parser.text
|
||||
XmlPullParser.START_TAG -> {
|
||||
if (tag == "audio-codecs")
|
||||
acc.audioCodec.clear()
|
||||
if (tag == "video-codecs")
|
||||
acc.videoCodec.clear()
|
||||
}
|
||||
XmlPullParser.END_TAG ->
|
||||
when (tag) {
|
||||
"outbound-proxy-1" ->
|
||||
if (text.isNotEmpty())
|
||||
acc.outbound.add(text)
|
||||
"outbound-proxy-2" ->
|
||||
if (text.isNotEmpty())
|
||||
acc.outbound.add(text)
|
||||
"registration-interval" ->
|
||||
acc.configuredRegInt = text.toInt()
|
||||
"register" ->
|
||||
acc.regint = if (text == "yes") acc.configuredRegInt else 0
|
||||
"audio-codec" ->
|
||||
if (text in audioCodecs)
|
||||
acc.audioCodec.add(text)
|
||||
"video-codec" ->
|
||||
if (text in videoCodecs)
|
||||
acc.videoCodec.add(text)
|
||||
"media-encoding" -> {
|
||||
val enc = text.lowercase(Locale.ROOT)
|
||||
if (enc in mediaEncMap.keys && enc.isNotEmpty())
|
||||
acc.mediaEnc = enc
|
||||
}
|
||||
"media-nat" -> {
|
||||
val nat = text.lowercase(Locale.ROOT)
|
||||
if (nat in mediaNatMap.keys && nat.isNotEmpty())
|
||||
acc.mediaNat = nat
|
||||
}
|
||||
"stun-turn-server" ->
|
||||
if (text.isNotEmpty())
|
||||
acc.stunServer = text
|
||||
"rtcp-mux" ->
|
||||
acc.rtcpMux = text == "yes"
|
||||
"100rel-mode" ->
|
||||
acc.rel100Mode = if (text == "yes")
|
||||
Api.REL100_ENABLED
|
||||
else
|
||||
Api.REL100_DISABLED
|
||||
"dtmf-mode" ->
|
||||
if (text in arrayOf("rtp-event", "sip-info", "auto"))
|
||||
acc.dtmfMode = when (text) {
|
||||
"rtp-event" -> Api.DTMFMODE_RTP_EVENT
|
||||
"sip-info" -> Api.DTMFMODE_SIP_INFO
|
||||
else -> Api.DTMFMODE_AUTO
|
||||
}
|
||||
"answer-mode" ->
|
||||
if (text in arrayOf("manual", "auto"))
|
||||
acc.answerMode = if (text == "manual")
|
||||
Api.ANSWERMODE_MANUAL
|
||||
else
|
||||
Api.ANSWERMODE_AUTO
|
||||
"redirect-mode" ->
|
||||
acc.autoRedirect = text == "yes"
|
||||
"voicemail-uri" ->
|
||||
if (text.isNotEmpty())
|
||||
acc.vmUri = text
|
||||
"country-code" ->
|
||||
acc.countryCode = text
|
||||
"tel-provider" ->
|
||||
acc.telProvider = text
|
||||
}
|
||||
}
|
||||
event = parser.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("accounts,$aor")
|
||||
setResult(Activity.RESULT_CANCELED, Intent())
|
||||
setResult(RESULT_CANCELED, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
@ -142,19 +473,11 @@ class AccountsActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
var accounts = ArrayList<AccountRow>()
|
||||
|
||||
fun generateAccounts() {
|
||||
accounts.clear()
|
||||
for (ua in BaresipService.uas)
|
||||
accounts.add(AccountRow(ua.account.aor.replace("sip:", ""),
|
||||
R.drawable.delete))
|
||||
}
|
||||
|
||||
fun saveAccounts() {
|
||||
var accounts = ""
|
||||
for (a in Account.accounts()) accounts = accounts + a.print() + "\n"
|
||||
Utils.putFileContents(BaresipService.filesPath + "/accounts", accounts.toByteArray(Charsets.UTF_8))
|
||||
Utils.putFileContents(BaresipService.filesPath + "/accounts",
|
||||
accounts.toByteArray(Charsets.UTF_8))
|
||||
// Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'")
|
||||
}
|
||||
|
||||
|
||||
@ -1,31 +1,64 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import com.tutpro.baresip.databinding.ActivityAndroidContactBinding
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
class AndroidContactActivity : AppCompatActivity() {
|
||||
class AndroidContactActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var binding: ActivityAndroidContactBinding
|
||||
private lateinit var layout: LinearLayout
|
||||
private lateinit var textAvatarView: TextView
|
||||
private lateinit var cardAvatarView: CardView
|
||||
private lateinit var cardImageAvatarView: ImageView
|
||||
private lateinit var nameView: TextView
|
||||
private lateinit var ulAdapter: AndroidUriListAdapter
|
||||
private lateinit var aor: String
|
||||
private lateinit var name: String
|
||||
|
||||
private var index = 0
|
||||
private var color = 0
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
@ -37,70 +70,207 @@ class AndroidContactActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAndroidContactBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
layout = binding.ContactView
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
index = intent.getIntExtra("index", 0)
|
||||
name = intent.getStringExtra("name")!!
|
||||
|
||||
textAvatarView = binding.TextAvatar
|
||||
cardAvatarView = binding.CardAvatar
|
||||
cardImageAvatarView = binding.ImageAvatar
|
||||
nameView = binding.Name
|
||||
val title: String = name
|
||||
val contact = Contact.androidContact(name)
|
||||
|
||||
val contact = Contact.contacts()[index] as Contact.AndroidContact
|
||||
val name = contact.name
|
||||
color = contact.color
|
||||
val thumbnailUri = contact.thumbnailUri
|
||||
if (thumbnailUri != null)
|
||||
showImageAvatar(thumbnailUri)
|
||||
else
|
||||
showTextAvatar(name, color)
|
||||
title = name
|
||||
nameView.text = name
|
||||
|
||||
val listView = binding.uris
|
||||
ulAdapter = AndroidUriListAdapter(this, contact.uris, aor)
|
||||
listView.adapter = ulAdapter
|
||||
|
||||
Utils.addActivity("android contact,$aor,$index")
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
if (BaresipService.activities.indexOf("android contact,$aor,$index") == -1)
|
||||
return true
|
||||
|
||||
when (item.itemId) {
|
||||
android.R.id.home ->
|
||||
goBack()
|
||||
if (contact == null) {
|
||||
Log.e(TAG, "No Android contact found with name $name")
|
||||
goBack()
|
||||
}
|
||||
|
||||
return true
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
ContactScreen(this, title, contact!!) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Utils.addActivity("android contact, $name")
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ContactScreen(ctx: Context, title: String, contact: Contact.AndroidContact,
|
||||
navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxHeight().imePadding().safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title, color = LocalCustomColors.current.grayLight,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
content = { contentPadding ->
|
||||
ContactContent(ctx, contentPadding, contact)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactContent(ctx: Context, contentPadding: PaddingValues, contact: Contact.AndroidContact) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Avatar(contact)
|
||||
ContactName()
|
||||
Uris(ctx, contact)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextAvatar(text: String, size: Int, color: Int) {
|
||||
Box(
|
||||
modifier = Modifier.size(size.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(SolidColor(Color(color)))
|
||||
}
|
||||
Text(text, fontSize = 72.sp, color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageAvatar(uri: Uri, size: Int) {
|
||||
AsyncImage(
|
||||
model = uri,
|
||||
contentDescription = stringResource(R.string.avatar_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(size.dp).clip(CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Avatar(contact: Contact.AndroidContact) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
color = contact.color
|
||||
val thumbnailUri = contact.thumbnailUri
|
||||
if (thumbnailUri != null)
|
||||
ImageAvatar(thumbnailUri, 96)
|
||||
else
|
||||
TextAvatar(if (name == "") "" else name[0].toString(), 96, color)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactName() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(name, fontSize = 24.sp, color = LocalCustomColors.current.itemText)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Uris(ctx: Context, contact: Contact.AndroidContact) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.background(LocalCustomColors.current.background),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(contact.uris) { uri ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = uri.substringAfter(":"),
|
||||
modifier = Modifier.weight(1f),
|
||||
fontSize = 18.sp,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.message),
|
||||
colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
|
||||
contentDescription = "Send Message",
|
||||
modifier = Modifier.padding(end = 24.dp).clickable {
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "message")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null) {
|
||||
Log.w(TAG, "message clickable did not find AoR $aor")
|
||||
} else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.call_small),
|
||||
colorFilter = ColorFilter.tint(LocalCustomColors.current.itemText),
|
||||
contentDescription = "Call",
|
||||
modifier = Modifier.clickable {
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "call")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null) {
|
||||
Log.w(TAG, "call clickable did not find AoR $aor")
|
||||
} else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("android contact,$index")
|
||||
setResult(Activity.RESULT_CANCELED, Intent(this, MainActivity::class.java))
|
||||
BaresipService.activities.remove("android contact,$name")
|
||||
setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun showTextAvatar(name: String, color: Int) {
|
||||
textAvatarView.visibility = View.VISIBLE
|
||||
cardAvatarView.visibility = View.GONE
|
||||
textAvatarView.background.setTint(color)
|
||||
textAvatarView.text = "${name[0]}"
|
||||
}
|
||||
|
||||
private fun showImageAvatar(thumbNailUri: Uri) {
|
||||
textAvatarView.visibility = View.GONE
|
||||
cardAvatarView.visibility = View.VISIBLE
|
||||
cardImageAvatarView.setImageURI(thumbNailUri)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
|
||||
class AndroidUriListAdapter(private val ctx: Context, private val rows: ArrayList<String>, val aor: String) :
|
||||
ArrayAdapter<String>(ctx, R.layout.android_uri_row, rows) {
|
||||
|
||||
private val layoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private class ViewHolder(view: View?) {
|
||||
val uriView: TextView = view?.findViewById(R.id.uri)!!
|
||||
val messageView: ImageButton = view?.findViewById(R.id.message)!!
|
||||
val callView: ImageButton = view?.findViewById(R.id.call)!!
|
||||
}
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
|
||||
val viewHolder: ViewHolder
|
||||
val rowView: View
|
||||
|
||||
if (view == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.android_uri_row, parent, false)
|
||||
viewHolder = ViewHolder(rowView)
|
||||
rowView.tag = viewHolder
|
||||
} else {
|
||||
rowView = view
|
||||
viewHolder = rowView.tag as ViewHolder
|
||||
}
|
||||
|
||||
val uri = rows[position]
|
||||
|
||||
viewHolder.uriView.text = uri.substringAfter(":")
|
||||
|
||||
viewHolder.messageView.setOnClickListener {
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "message")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null) {
|
||||
Log.w(TAG, "onClickListener did not find AoR $aor")
|
||||
} else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
|
||||
viewHolder.callView.setOnClickListener {
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
i.putExtra("action", "call")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null) {
|
||||
Log.w(TAG, "onClickListener did not find AoR $aor")
|
||||
} else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
|
||||
return rowView
|
||||
}
|
||||
}
|
||||
@ -121,6 +121,6 @@ object Api {
|
||||
external fun module_load(module: String): Int
|
||||
external fun module_unload(module: String)
|
||||
|
||||
external fun create_AAudio_SessionId(): Int
|
||||
|
||||
external fun AAudio_open_stream(): Int
|
||||
external fun AAudio_close_stream()
|
||||
}
|
||||
|
||||
67
app/src/main/kotlin/com/tutpro/baresip/AppTheme.kt
Normal file
67
app/src/main/kotlin/com/tutpro/baresip/AppTheme.kt
Normal file
@ -0,0 +1,67 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val darkTheme = remember { BaresipService.darkTheme }
|
||||
|
||||
// "normal" palette, nothing change here
|
||||
val colorScheme = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme.value) dynamicDarkColorScheme(context = LocalContext.current)
|
||||
else dynamicLightColorScheme(context = LocalContext.current)
|
||||
}
|
||||
darkTheme.value -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
}
|
||||
|
||||
// logic for which custom palette to use
|
||||
val customColorsPalette =
|
||||
if (darkTheme.value) DarkCustomColors
|
||||
else LightCustomColors
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
val decorView = window.decorView
|
||||
|
||||
// Ensure insets are applied correctly
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
// Handle the status bar appearance
|
||||
val insetsController = WindowCompat.getInsetsController(window, decorView)
|
||||
insetsController.apply {
|
||||
isAppearanceLightStatusBars = !darkTheme.value
|
||||
isAppearanceLightNavigationBars = !darkTheme.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// here is the important point, where you will expose custom objects
|
||||
CompositionLocalProvider(
|
||||
LocalCustomColors provides customColorsPalette // our custom palette
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme, // the MaterialTheme still uses the "normal" palette
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
704
app/src/main/kotlin/com/tutpro/baresip/BaresipContactActivity.kt
Normal file
704
app/src/main/kotlin/com/tutpro/baresip/BaresipContactActivity.kt
Normal file
@ -0,0 +1,704 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.CommonDataKinds
|
||||
import android.provider.ContactsContract.Contacts.Data
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.tutpro.baresip.CustomElements.Checkbox
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
|
||||
class BaresipContactActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var name: String
|
||||
private lateinit var uri: String
|
||||
private var new = false
|
||||
private var favorite = false
|
||||
private var android = false
|
||||
private var avatarImage: Bitmap? = null
|
||||
private var newName = ""
|
||||
private var newUri = ""
|
||||
private var newFavorite = false
|
||||
private var newAndroid = false
|
||||
private var uriOrName = ""
|
||||
private var color = 0
|
||||
private var id: Long = 0
|
||||
private var newAvatar = ""
|
||||
private var tmpFile: File? = null
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
val title: String
|
||||
|
||||
new = intent.getBooleanExtra("new", false)
|
||||
|
||||
if (new) {
|
||||
name = ""
|
||||
uri = intent.getStringExtra("uri")!!
|
||||
favorite = false
|
||||
avatarImage = null
|
||||
android = BaresipService.contactsMode == "android"
|
||||
title = getString(R.string.new_contact)
|
||||
uriOrName = uri
|
||||
color = Utils.randomColor()
|
||||
id = System.currentTimeMillis()
|
||||
} else {
|
||||
name = intent.getStringExtra("name")!!
|
||||
val contact = Contact.baresipContact(name)!!
|
||||
uri = contact.uri
|
||||
favorite = contact.favorite
|
||||
avatarImage = contact.avatarImage
|
||||
android = false
|
||||
title = name
|
||||
uriOrName = name
|
||||
color = contact.color
|
||||
id = contact.id
|
||||
}
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
ContactScreen(this, title) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Utils.addActivity("baresip contact,$new,$uriOrName")
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxHeight().imePadding().safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = { TopAppBar(ctx, title, navigateBack) },
|
||||
content = { contentPadding ->
|
||||
ContactContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopAppBar(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.grayLight,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
checkOnClick(ctx)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
tint = LocalCustomColors.current.light,
|
||||
contentDescription = "Check"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ContactContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
|
||||
var avatarType by remember { mutableStateOf(if (avatarImage != null) "image" else "text") }
|
||||
var textAvatarText by remember { mutableStateOf("") }
|
||||
var textAvatarColor by remember { mutableIntStateOf(0) }
|
||||
var imageAvatarUri by remember { mutableStateOf("") }
|
||||
|
||||
val avatarRequest =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
if (it != null)
|
||||
try {
|
||||
val inputStream = baseContext.contentResolver.openInputStream(it)
|
||||
val avatarBitmap = BitmapFactory.decodeStream(inputStream)
|
||||
inputStream?.close()
|
||||
val scaledBitmap = avatarBitmap.scale(192, 192)
|
||||
val exif = ExifInterface(baseContext.contentResolver.openInputStream(it)!!)
|
||||
val orientation = exif.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL
|
||||
)
|
||||
val rotatedBitmap = rotateBitmap(scaledBitmap, orientation)
|
||||
tmpFile = File(
|
||||
BaresipService.filesPath + "/tmp",
|
||||
"${System.currentTimeMillis()}.png"
|
||||
)
|
||||
if (Utils.saveBitmap(rotatedBitmap, tmpFile!!)) {
|
||||
newAvatar = "image"
|
||||
avatarType = newAvatar
|
||||
imageAvatarUri = Uri.fromFile(tmpFile).toString()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Could not read avatar image: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextAvatar(size: Int) {
|
||||
Box(
|
||||
modifier = Modifier.size(size.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(SolidColor(Color(textAvatarColor)))
|
||||
}
|
||||
textAvatarText = if (name == "") "" else name[0].toString()
|
||||
Text(textAvatarText, fontSize = 72.sp, color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageAvatar(size: Int) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(model = imageAvatarUri),
|
||||
contentDescription = stringResource(R.string.avatar_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(size.dp).clip(CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun avatar() {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
if (new) {
|
||||
textAvatarText = "?"
|
||||
textAvatarColor = color
|
||||
} else {
|
||||
val avatarImage = avatarImage
|
||||
val avatarFile = File(BaresipService.filesPath, "${id}.png")
|
||||
if (avatarImage != null && (newAvatar == "" || newAvatar == "image")) {
|
||||
imageAvatarUri = if (tmpFile != null && tmpFile!!.exists())
|
||||
Uri.fromFile(tmpFile).toString()
|
||||
else
|
||||
Uri.fromFile(avatarFile).toString()
|
||||
}
|
||||
else {
|
||||
textAvatarText = "${name[0]}"
|
||||
textAvatarColor = color
|
||||
}
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(96.dp)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
avatarRequest.launch("image/*")
|
||||
},
|
||||
onLongClick = {
|
||||
textAvatarColor = Utils.randomColor()
|
||||
color = textAvatarColor
|
||||
newAvatar = "text"
|
||||
avatarType = newAvatar
|
||||
}
|
||||
),
|
||||
) {
|
||||
if (avatarType == "text")
|
||||
TextAvatar(96)
|
||||
else
|
||||
ImageAvatar(96)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun contactName() {
|
||||
var contactName by remember { mutableStateOf(name) }
|
||||
newName = contactName
|
||||
OutlinedTextField(
|
||||
value = contactName,
|
||||
placeholder = { Text(stringResource(R.string.contact_name)) },
|
||||
onValueChange = {
|
||||
contactName = it
|
||||
newName = contactName
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText
|
||||
),
|
||||
label = { Text(stringResource(R.string.contact_name)) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun contactUri() {
|
||||
var contactUri by remember { mutableStateOf(uri) }
|
||||
newUri = contactUri
|
||||
OutlinedTextField(
|
||||
value = contactUri,
|
||||
placeholder = { Text(stringResource(R.string.user_domain_or_number)) },
|
||||
onValueChange = {
|
||||
contactUri = it
|
||||
newUri = contactUri
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(
|
||||
fontSize = 18.sp, color = LocalCustomColors.current.itemText
|
||||
),
|
||||
label = { Text(stringResource(R.string.sip_or_tel_uri)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun favorite(ctx: Context) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.favorite),
|
||||
modifier = Modifier.weight(1f)
|
||||
.clickable {
|
||||
Utils.alertView(
|
||||
ctx, getString(R.string.favorite), getString(R.string.favorite_help)
|
||||
)
|
||||
},
|
||||
color = LocalCustomColors.current.itemText,
|
||||
)
|
||||
var favoriteContact by remember { mutableStateOf(favorite) }
|
||||
newFavorite = favoriteContact
|
||||
Checkbox(
|
||||
checked = favoriteContact,
|
||||
onCheckedChange = {
|
||||
favoriteContact = it
|
||||
newFavorite = favoriteContact
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun android() {
|
||||
if (new && BaresipService.contactsMode != "baresip")
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.android),
|
||||
modifier = Modifier.weight(1f),
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
var androidContact by remember { mutableStateOf(android) }
|
||||
newAndroid = androidContact
|
||||
Checkbox(
|
||||
checked = androidContact,
|
||||
onCheckedChange = {
|
||||
if (BaresipService.contactsMode != "android") {
|
||||
androidContact = it
|
||||
newAndroid = androidContact
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
avatar()
|
||||
contactName()
|
||||
contactUri()
|
||||
favorite(ctx)
|
||||
android()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOnClick(ctx: Context) {
|
||||
if (BaresipService.activities.indexOf("baresip contact,$new,$uriOrName") == -1)
|
||||
return
|
||||
|
||||
newUri = newUri.filterNot{setOf('-', ' ', '(', ')').contains(it)}
|
||||
if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:"))
|
||||
newUri = if (Utils.isTelNumber(newUri))
|
||||
"tel:$newUri"
|
||||
else
|
||||
"sip:$newUri"
|
||||
if (!Utils.checkUri(newUri)) {
|
||||
Utils.alertView(
|
||||
ctx, getString(R.string.notice),
|
||||
String.format(getString(R.string.invalid_sip_or_tel_uri), newUri)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
newName = newName.trim()
|
||||
if (newName == "") newName = newUri.substringAfter(":")
|
||||
if (!Utils.checkName(newName)) {
|
||||
Utils.alertView(
|
||||
ctx, getString(R.string.notice),
|
||||
String.format(getString(R.string.invalid_contact), newName)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val alert: Boolean = if (new)
|
||||
Contact.nameExists(newName, BaresipService.contacts,true)
|
||||
else {
|
||||
(uriOrName != newName) && Contact.nameExists(newName, BaresipService.contacts, false)
|
||||
}
|
||||
if (alert) {
|
||||
Utils.alertView(
|
||||
ctx, getString(R.string.notice),
|
||||
String.format(getString(R.string.contact_already_exists), newName))
|
||||
return
|
||||
}
|
||||
|
||||
val contact: Contact.BaresipContact
|
||||
|
||||
if (new) {
|
||||
contact = Contact.BaresipContact(newName, newUri, color, id, newFavorite)
|
||||
} else {
|
||||
contact = Contact.baresipContact(name)!!
|
||||
contact.uri = newUri
|
||||
contact.name = newName
|
||||
contact.color = color
|
||||
contact.favorite = newFavorite
|
||||
}
|
||||
|
||||
when (newAvatar) {
|
||||
"text" -> {
|
||||
if (contact.avatarImage != null) {
|
||||
contact.avatarImage = null
|
||||
Utils.deleteFile(File(BaresipService.filesPath, "${contact.id}.png"))
|
||||
}
|
||||
}
|
||||
"image" -> {
|
||||
val imageFilePath = BaresipService.filesPath + "/${contact.id}.png"
|
||||
val imageFile = File(imageFilePath)
|
||||
tmpFile!!.copyTo(target = imageFile, overwrite = true)
|
||||
contact.avatarImage = BitmapFactory.decodeFile(imageFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (newAndroid)
|
||||
addOrUpdateAndroidContact(ctx, contact)
|
||||
else {
|
||||
if (new)
|
||||
Contact.addBaresipContact(contact)
|
||||
else
|
||||
Contact.updateBaresipContact(contact)
|
||||
}
|
||||
|
||||
Contact.contactsUpdate()
|
||||
|
||||
Contact.saveBaresipContacts()
|
||||
|
||||
BaresipService.activities.remove("baresip contact,$new,$uriOrName")
|
||||
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
if (newAndroid && contact.favorite)
|
||||
i.putExtra("name", newName)
|
||||
setResult(RESULT_OK, i)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap {
|
||||
val matrix = Matrix()
|
||||
when (orientation) {
|
||||
ExifInterface.ORIENTATION_NORMAL -> return bitmap
|
||||
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
|
||||
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
|
||||
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
|
||||
matrix.setRotate(180f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
||||
matrix.setRotate(90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
|
||||
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
||||
matrix.setRotate(-90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
|
||||
else -> return bitmap
|
||||
}
|
||||
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
|
||||
bitmap.width, bitmap.height, matrix, true)
|
||||
bitmap.recycle()
|
||||
return rotatedBitmap
|
||||
}
|
||||
|
||||
private fun addOrUpdateAndroidContact(ctx: Context, contact: Contact.BaresipContact) {
|
||||
val projection = arrayOf(ContactsContract.Data.RAW_CONTACT_ID)
|
||||
val selection = ContactsContract.Data.MIMETYPE + "='" +
|
||||
CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " +
|
||||
CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'"
|
||||
val c: Cursor? = ctx.contentResolver.query(
|
||||
ContactsContract.Data.CONTENT_URI, projection,
|
||||
selection, null, null)
|
||||
if (c != null && c.moveToFirst()) {
|
||||
updateAndroidContact(c.getLong(0), contact)
|
||||
} else {
|
||||
addAndroidContact(ctx, contact)
|
||||
}
|
||||
c?.close()
|
||||
}
|
||||
|
||||
private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Boolean {
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(
|
||||
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null).build())
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
|
||||
.build())
|
||||
val mimeType = if (contact.uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, mimeType)
|
||||
.withValue(Data.DATA1, contact.uri.substringAfter(":"))
|
||||
.build())
|
||||
|
||||
if (contact.avatarImage != null) {
|
||||
val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!)
|
||||
if (photoData != null) {
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.Photo.PHOTO, photoData)
|
||||
.build())
|
||||
}
|
||||
}
|
||||
try {
|
||||
ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of contact ${contact.name} failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateAndroidContact(rawContactId: Long, contact: Contact.BaresipContact) {
|
||||
if (updateAndroidUri(rawContactId, contact.uri) == 0)
|
||||
addAndroidUri(rawContactId, contact.uri)
|
||||
if (updateAndroidPhoto(rawContactId, contact.avatarImage) == 0)
|
||||
if (contact.avatarImage != null)
|
||||
addAndroidPhoto(rawContactId, contact.avatarImage!!)
|
||||
}
|
||||
|
||||
private fun addAndroidUri(rawContactId: Long, uri: String) {
|
||||
val mimeType = if (uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
||||
.withValue(Data.MIMETYPE, mimeType)
|
||||
.withValue(Data.DATA1, uri.substringAfter(":"))
|
||||
.build())
|
||||
try {
|
||||
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of SIP URI $uri failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAndroidUri(rawContactId: Long, uri: String): Int {
|
||||
val mimeType = if (uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(ContactsContract.Data.DATA1, uri)
|
||||
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
||||
"${ContactsContract.Data.MIMETYPE}='$mimeType'"
|
||||
return try {
|
||||
contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Update of Android URI $uri failed")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAndroidPhoto(rawContactId: Long, photoBits: Bitmap) {
|
||||
val photoBytes = bitmapToPNGByteArray(photoBits)
|
||||
if (photoBytes != null) {
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(
|
||||
ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.Photo.PHOTO, photoBytes)
|
||||
.build())
|
||||
try {
|
||||
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of Android photo failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAndroidPhoto(rawContactId: Long, photoBits: Bitmap?): Int {
|
||||
val photoBytes = if (photoBits == null)
|
||||
null
|
||||
else
|
||||
bitmapToPNGByteArray(photoBits)
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes)
|
||||
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
||||
"${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'"
|
||||
return try {
|
||||
contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "updateAndroidPhoto failed")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun bitmapToPNGByteArray(bitmap: Bitmap): ByteArray? {
|
||||
val size = bitmap.width * bitmap.height * 4
|
||||
val out = ByteArrayOutputStream(size)
|
||||
return try {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
out.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to serialize photo: $e")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("baresip contact,$new,$uriOrName")
|
||||
setResult(RESULT_CANCELED, Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,11 @@ package com.tutpro.baresip
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.*
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothHeadset
|
||||
import android.bluetooth.BluetoothManager
|
||||
@ -13,20 +17,37 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
||||
import android.content.res.Configuration
|
||||
import android.database.ContentObserver
|
||||
import android.media.*
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.MODE_IN_COMMUNICATION
|
||||
import android.media.AudioManager.MODE_NORMAL
|
||||
import android.media.AudioManager.RINGER_MODE_SILENT
|
||||
import android.media.MediaPlayer
|
||||
import android.media.Ringtone
|
||||
import android.media.RingtoneManager
|
||||
import android.media.audiofx.AcousticEchoCanceler
|
||||
import android.media.audiofx.AutomaticGainControl
|
||||
import android.media.audiofx.NoiseSuppressor
|
||||
import android.net.*
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.*
|
||||
import android.os.Build.VERSION
|
||||
import android.os.CountDownTimer
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.Settings
|
||||
import android.system.OsConstants
|
||||
import android.telecom.TelecomManager
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
@ -37,7 +58,10 @@ import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -49,7 +73,9 @@ import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@ -99,7 +125,7 @@ class BaresipService: Service() {
|
||||
filesPath = filesDir.absolutePath
|
||||
pName = packageName
|
||||
|
||||
am = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am = applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
|
||||
val ntUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext,
|
||||
RingtoneManager.TYPE_NOTIFICATION)
|
||||
@ -113,14 +139,14 @@ class BaresipService: Service() {
|
||||
createNotificationChannels()
|
||||
snb = NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID)
|
||||
|
||||
pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
pm = getSystemService(POWER_SERVICE) as PowerManager
|
||||
|
||||
vibrator = if (VERSION.SDK_INT >= 31) {
|
||||
val vibratorManager = applicationContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
val vibratorManager = applicationContext.getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
vibratorManager.defaultVibrator
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
applicationContext.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator
|
||||
applicationContext.getSystemService(VIBRATOR_SERVICE) as Vibrator
|
||||
}
|
||||
|
||||
// This is needed to keep service running also in Doze Mode
|
||||
@ -133,7 +159,7 @@ class BaresipService: Service() {
|
||||
}
|
||||
}
|
||||
|
||||
cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val builder = NetworkRequest.Builder()
|
||||
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
cm.registerNetworkCallback(
|
||||
@ -182,7 +208,7 @@ class BaresipService: Service() {
|
||||
}
|
||||
)
|
||||
|
||||
wm = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
wm = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
|
||||
hotSpotIsEnabled = Utils.isHotSpotOn(wm)
|
||||
|
||||
hotSpotReceiver = object : BroadcastReceiver() {
|
||||
@ -238,9 +264,9 @@ class BaresipService: Service() {
|
||||
this.registerReceiver(hotSpotReceiver,
|
||||
IntentFilter("android.net.wifi.WIFI_AP_STATE_CHANGED"))
|
||||
|
||||
tm = getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
||||
tm = getSystemService(TELECOM_SERVICE) as TelecomManager
|
||||
|
||||
btm = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
btm = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
|
||||
btAdapter = btm.adapter
|
||||
|
||||
proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
||||
@ -530,10 +556,7 @@ class BaresipService: Service() {
|
||||
if (ua == null)
|
||||
Log.w(TAG, "onStartCommand did not find UA $uap")
|
||||
else
|
||||
ChatsActivity.saveUaMessage(
|
||||
ua.account.aor,
|
||||
intent.getStringExtra("time")!!.toLong()
|
||||
)
|
||||
Message.updateAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
|
||||
nm.cancel(MESSAGE_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
@ -543,10 +566,8 @@ class BaresipService: Service() {
|
||||
if (ua == null)
|
||||
Log.w(TAG, "onStartCommand did not find UA $uap")
|
||||
else
|
||||
ChatsActivity.deleteUaMessage(
|
||||
ua.account.aor,
|
||||
intent.getStringExtra("time")!!.toLong()
|
||||
)
|
||||
Message.deleteAorMessage(ua.account.aor, intent.getStringExtra("time")!!.toLong())
|
||||
|
||||
nm.cancel(MESSAGE_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
@ -571,6 +592,20 @@ class BaresipService: Service() {
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
val currentNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
when (currentNightMode) {
|
||||
Configuration.UI_MODE_NIGHT_NO -> {
|
||||
darkTheme.value = Preferences(applicationContext).displayTheme ==
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
}
|
||||
Configuration.UI_MODE_NIGHT_YES -> {
|
||||
darkTheme.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
@ -598,7 +633,7 @@ class BaresipService: Service() {
|
||||
R.drawable.circle_white
|
||||
else
|
||||
R.drawable.circle_yellow
|
||||
uas.add(ua)
|
||||
ua.add()
|
||||
|
||||
val acc = ua.account
|
||||
if (acc.authUser != "" && acc.authPass == NO_AUTH_PASS) {
|
||||
@ -629,6 +664,47 @@ class BaresipService: Service() {
|
||||
if (ev[0] == "recorder sessionid") {
|
||||
recorderSessionId = ev[1].toInt()
|
||||
Log.d(TAG, "got recorder sessionid $recorderSessionId")
|
||||
if (recorderSessionId != 0) {
|
||||
if (!webrtcAec) {
|
||||
aec = AcousticEchoCanceler.create(recorderSessionId)
|
||||
if (aec != null) {
|
||||
if (!aec!!.getEnabled()) {
|
||||
aec!!.setEnabled(true)
|
||||
if (aec!!.getEnabled())
|
||||
Log.d(TAG, "AEC is enabled")
|
||||
else
|
||||
Log.w(TAG, "Failed to enable AEC")
|
||||
}
|
||||
} else
|
||||
Log.w(
|
||||
TAG, "Failed to create AEC for session " +
|
||||
"$recorderSessionId"
|
||||
)
|
||||
}
|
||||
if (agcAvailable) {
|
||||
agc = AutomaticGainControl.create(recorderSessionId)
|
||||
if (agc != null) {
|
||||
if (!agc!!.getEnabled()) {
|
||||
agc!!.setEnabled(true)
|
||||
if (agc!!.getEnabled())
|
||||
Log.d(TAG, "AGC is enabled")
|
||||
}
|
||||
} else
|
||||
Log.w(TAG, "Failed to create AGC")
|
||||
}
|
||||
if (nsAvailable) {
|
||||
ns = NoiseSuppressor.create(recorderSessionId)
|
||||
if (ns != null) {
|
||||
if (!ns!!.getEnabled()) {
|
||||
ns!!.setEnabled(true)
|
||||
if (ns!!.getEnabled())
|
||||
Log.d(TAG, "NS is enabled")
|
||||
}
|
||||
} else
|
||||
Log.w(TAG, "Failed to create NS")
|
||||
}
|
||||
recorderSessionId = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -649,31 +725,32 @@ class BaresipService: Service() {
|
||||
return
|
||||
}
|
||||
|
||||
for (accountIndex in uas.indices) {
|
||||
if (uas[accountIndex].account.aor == aor) {
|
||||
for (accountIndex in uas.value.indices) {
|
||||
if (uas.value[accountIndex].account.aor == aor) {
|
||||
when (ev[0]) {
|
||||
"registering", "unregistering" -> {
|
||||
ua.status = R.drawable.circle_yellow
|
||||
updateStatusNotification()
|
||||
ua.uaUpdateStatus(R.drawable.circle_yellow)
|
||||
if (isMainVisible)
|
||||
registrationUpdate.postValue(System.currentTimeMillis())
|
||||
return
|
||||
}
|
||||
"registered" -> {
|
||||
ua.status = if (Api.account_regint(ua.account.accp) == 0)
|
||||
R.drawable.circle_white
|
||||
else
|
||||
R.drawable.circle_green
|
||||
ua.uaUpdateStatus(
|
||||
if (Api.account_regint(ua.account.accp) == 0)
|
||||
R.drawable.circle_white
|
||||
else
|
||||
R.drawable.circle_green)
|
||||
updateStatusNotification()
|
||||
if (isMainVisible)
|
||||
registrationUpdate.postValue(System.currentTimeMillis())
|
||||
return
|
||||
}
|
||||
"registering failed" -> {
|
||||
ua.status = if (Api.account_regint(ua.account.accp) == 0)
|
||||
ua.uaUpdateStatus(if (Api.account_regint(ua.account.accp) == 0)
|
||||
R.drawable.circle_white
|
||||
else
|
||||
R.drawable.circle_red
|
||||
R.drawable.circle_red)
|
||||
updateStatusNotification()
|
||||
if (isMainVisible)
|
||||
registrationUpdate.postValue(System.currentTimeMillis())
|
||||
@ -752,7 +829,7 @@ class BaresipService: Service() {
|
||||
"call incoming" -> {
|
||||
val peerUri = ev[1]
|
||||
Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
|
||||
Call(callp, ua, peerUri, "in", "incoming", Utils.dtmfWatcher(callp)).add()
|
||||
Call(callp, ua, peerUri, "in", "incoming", null).add()
|
||||
if (speakerPhone && !Utils.isSpeakerPhoneOn(am))
|
||||
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am)
|
||||
if (ua.account.answerMode == Api.ANSWERMODE_MANUAL) {
|
||||
@ -827,47 +904,6 @@ class BaresipService: Service() {
|
||||
if (am.mode != MODE_IN_COMMUNICATION)
|
||||
am.mode = MODE_IN_COMMUNICATION
|
||||
call!!.status = "connected"
|
||||
if (recorderSessionId != 0) {
|
||||
if (Utils.isAecSupported() && !webrtcAec) {
|
||||
aec = AcousticEchoCanceler.create(recorderSessionId)
|
||||
if (aec != null) {
|
||||
if (!aec!!.getEnabled()) {
|
||||
aec!!.setEnabled(true)
|
||||
if (aec!!.getEnabled())
|
||||
Log.d(TAG, "AEC is enabled")
|
||||
else
|
||||
Log.w(TAG, "Failed to enable AEC")
|
||||
}
|
||||
} else
|
||||
Log.w(
|
||||
TAG, "Failed to create AEC for session " +
|
||||
"$recorderSessionId"
|
||||
)
|
||||
}
|
||||
if (agcAvailable) {
|
||||
agc = AutomaticGainControl.create(recorderSessionId)
|
||||
if (agc != null) {
|
||||
if (!agc!!.getEnabled()) {
|
||||
agc!!.setEnabled(true)
|
||||
if (agc!!.getEnabled())
|
||||
Log.d(TAG, "AGC is enabled")
|
||||
}
|
||||
} else
|
||||
Log.w(TAG, "Failed to create AGC")
|
||||
}
|
||||
if (nsAvailable) {
|
||||
ns = NoiseSuppressor.create(recorderSessionId)
|
||||
if (ns != null) {
|
||||
if (!ns!!.getEnabled()) {
|
||||
ns!!.setEnabled(true)
|
||||
if (ns!!.getEnabled())
|
||||
Log.d(TAG, "ns is enabled")
|
||||
}
|
||||
} else
|
||||
Log.w(TAG, "Failed to create NS")
|
||||
}
|
||||
recorderSessionId = 0
|
||||
}
|
||||
call.onhold = false
|
||||
if (ua.account.callHistory)
|
||||
call.startTime = GregorianCalendar()
|
||||
@ -951,8 +987,11 @@ class BaresipService: Service() {
|
||||
stopRinging()
|
||||
stopMediaPlayer()
|
||||
aec?.release()
|
||||
aec = null
|
||||
agc?.release()
|
||||
agc = null
|
||||
ns?.release()
|
||||
ns = null
|
||||
val newCall = call.newCall
|
||||
if (newCall != null) {
|
||||
newCall.onHoldCall = null
|
||||
@ -1237,14 +1276,14 @@ class BaresipService: Service() {
|
||||
2-> R.id.status2
|
||||
else -> R.id.status3
|
||||
}
|
||||
if (i < uas.size) {
|
||||
notificationLayout.setImageViewResource(resId, uas[i].status)
|
||||
if (i < uas.value.size) {
|
||||
notificationLayout.setImageViewResource(resId, uas.value[i].status)
|
||||
notificationLayout.setViewVisibility(resId, View.VISIBLE)
|
||||
} else {
|
||||
notificationLayout.setViewVisibility(resId, View.INVISIBLE)
|
||||
}
|
||||
}
|
||||
if (uas.size > 4)
|
||||
if (uas.value.size > 4)
|
||||
notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE)
|
||||
else
|
||||
notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE)
|
||||
@ -1467,7 +1506,7 @@ class BaresipService: Service() {
|
||||
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) {
|
||||
val props = cm.getLinkProperties(n) ?: continue
|
||||
for (la in props.linkAddresses)
|
||||
if (la.scope == android.system.OsConstants.RT_SCOPE_UNIVERSE &&
|
||||
if (la.scope == OsConstants.RT_SCOPE_UNIVERSE &&
|
||||
props.interfaceName != null && la.address.hostAddress != null &&
|
||||
afMatch(la.address.hostAddress!!))
|
||||
addresses[la.address.hostAddress!!] = props.interfaceName!!
|
||||
@ -1544,9 +1583,10 @@ class BaresipService: Service() {
|
||||
stopRinging()
|
||||
stopMediaPlayer()
|
||||
abandonAudioFocus(applicationContext)
|
||||
uas.clear()
|
||||
uas.value = emptyList()
|
||||
uasStatus.value = emptyMap()
|
||||
callHistory.clear()
|
||||
messages.clear()
|
||||
messages = emptyList()
|
||||
if (this::nm.isInitialized)
|
||||
nm.cancelAll()
|
||||
if (this::partialWakeLock.isInitialized && partialWakeLock.isHeld)
|
||||
@ -1590,17 +1630,20 @@ class BaresipService: Service() {
|
||||
var isRecOn = false
|
||||
var toneCountry = "us"
|
||||
|
||||
val uas = ArrayList<UserAgent>()
|
||||
val uas = mutableStateOf(emptyList<UserAgent>())
|
||||
val uasStatus = mutableStateOf(emptyMap<String, Int>())
|
||||
var contacts by mutableStateOf(emptyList<Contact>())
|
||||
val darkTheme = mutableStateOf(false)
|
||||
var messages by mutableStateOf(emptyList<Message>())
|
||||
|
||||
val calls = ArrayList<Call>()
|
||||
var callHistory = ArrayList<CallHistoryNew>()
|
||||
var messages = ArrayList<Message>()
|
||||
val messageUpdate = MutableLiveData<Long>()
|
||||
val contactUpdate = MutableLiveData<Long>()
|
||||
val registrationUpdate = MutableLiveData<Long>()
|
||||
val baresipContacts = ArrayList<Contact.BaresipContact>()
|
||||
val androidContacts = ArrayList<Contact.AndroidContact>()
|
||||
val contacts = ArrayList<Contact>()
|
||||
val contactNames = ArrayList<String>()
|
||||
val baresipContacts = mutableStateOf(emptyList<Contact.BaresipContact>())
|
||||
val androidContacts = mutableStateOf(emptyList<Contact.AndroidContact>())
|
||||
val contactNames = mutableStateOf(emptyList<String>())
|
||||
var contactsMode = "baresip"
|
||||
val chatTexts: MutableMap<String, String> = mutableMapOf()
|
||||
val activities = mutableListOf<String>()
|
||||
@ -1611,11 +1654,12 @@ class BaresipService: Service() {
|
||||
// <aor, password> of those accounts that have auth username without auth password
|
||||
val aorPasswords = mutableMapOf<String, String>()
|
||||
var audioFocusRequest: AudioFocusRequestCompat? = null
|
||||
var agcAvailable = AutomaticGainControl.isAvailable()
|
||||
private val nsAvailable = NoiseSuppressor.isAvailable()
|
||||
var aecAvailable = false
|
||||
private var aec: AcousticEchoCanceler? = null
|
||||
private var ns: NoiseSuppressor? = null
|
||||
var agcAvailable = false
|
||||
private var agc: AutomaticGainControl? = null
|
||||
private val nsAvailable = NoiseSuppressor.isAvailable()
|
||||
private var ns: NoiseSuppressor? = null
|
||||
var webrtcAec = false
|
||||
private var btAdapter: BluetoothAdapter? = null
|
||||
private var recorderSessionId = 0
|
||||
@ -1626,7 +1670,7 @@ class BaresipService: Service() {
|
||||
Log.d(TAG, "Already focused")
|
||||
return true
|
||||
}
|
||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
val attributes = AudioAttributesCompat.Builder()
|
||||
.setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION)
|
||||
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
|
||||
@ -1647,7 +1691,7 @@ class BaresipService: Service() {
|
||||
}
|
||||
|
||||
fun abandonAudioFocus(ctx: Context) {
|
||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
if (audioFocusRequest != null) {
|
||||
Log.d(TAG, "Abandoning audio focus")
|
||||
if (AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!) ==
|
||||
@ -1683,7 +1727,7 @@ class BaresipService: Service() {
|
||||
}
|
||||
|
||||
private fun startBluetoothSco(ctx: Context, delay: Long, count: Int) {
|
||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
if (isBluetoothScoOn(am)) {
|
||||
Log.d(TAG, "Bluetooth SCO is already on")
|
||||
return
|
||||
@ -1705,7 +1749,7 @@ class BaresipService: Service() {
|
||||
|
||||
private fun stopBluetoothSco(ctx: Context) {
|
||||
Log.d(TAG, "Stopping Bluetooth SCO")
|
||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
if (!isBluetoothScoOn(am)) {
|
||||
Log.d(TAG, "Bluetooth SCO is already off")
|
||||
return
|
||||
@ -1719,7 +1763,5 @@ class BaresipService: Service() {
|
||||
am.isBluetoothScoOn = false
|
||||
}, 100)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,24 +1,67 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import com.tutpro.baresip.databinding.ActivityCallDetailsBinding
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.net.toUri
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tutpro.baresip.CallsActivity.Companion.uaHistory
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.DateFormat
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
class CallDetailsActivity : AppCompatActivity() {
|
||||
class CallDetailsActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var binding: ActivityCallDetailsBinding
|
||||
private lateinit var aor: String
|
||||
private lateinit var peer: String
|
||||
private lateinit var account: Account
|
||||
private lateinit var callRow: CallRow
|
||||
private lateinit var details: ArrayList<CallRow. Details>
|
||||
private val decPlayer = MediaPlayer()
|
||||
private val encPlayer = MediaPlayer()
|
||||
private var position = 0
|
||||
@ -33,38 +76,282 @@ class CallDetailsActivity : AppCompatActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_call_details)
|
||||
enableEdgeToEdge()
|
||||
|
||||
binding = ActivityCallDetailsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
if (Build.VERSION.SDK_INT >= 35)
|
||||
binding.CallDetailsView.updatePadding(top = systemBars.top + 56)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
if (!Utils.isDarkTheme(this))
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
account = Account.ofAor(aor)!!
|
||||
peer = intent.getStringExtra("peer")!!
|
||||
position = intent.getIntExtra("position", 0)
|
||||
callRow = uaHistory.value[position]
|
||||
details = callRow.details
|
||||
|
||||
Utils.addActivity("call_details,$aor,$peer,$position")
|
||||
|
||||
val headerView = binding.peer
|
||||
val headerText = "${getString(R.string.peer)} $peer"
|
||||
headerView.text = headerText
|
||||
|
||||
val listView = binding.calls
|
||||
listView.adapter = CallDetailsAdapter(this, CallsActivity.uaHistory[position].details,
|
||||
decPlayer, encPlayer)
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
CallDetailsScreen(this, getString(R.string.call_details)) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CallDetailsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
content = { contentPadding ->
|
||||
CallDetailsContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallDetailsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(top = 16.dp, start = 16.dp, end = 4.dp, bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Peer(ctx, peer, account)
|
||||
Calls(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Peer(ctx: Context, peer: String, account: Account) {
|
||||
val headerText = getString(R.string.peer) + " " +
|
||||
Utils.friendlyUri(ctx, peer, account)
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Calls(ctx: Context) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = getString(R.string.direction),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.width(96.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(text = getString(R.string.time),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text = getString(R.string.calls_duration),
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
color = LocalCustomColors.current.itemText,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
)
|
||||
.background(LocalCustomColors.current.background),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(details) { detail ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(detail.direction),
|
||||
contentDescription = "Direction",
|
||||
modifier = Modifier.width(64.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(38.dp))
|
||||
val durationText = startTime(ctx, detail)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Duration(ctx, detail, durationText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun startTime(ctx: Context, detail: CallRow.Details): String {
|
||||
val startTime = detail.startTime
|
||||
val stopTime = detail.stopTime
|
||||
val startTimeText: String
|
||||
val durationText: String
|
||||
val stopText = if (DateUtils.isToday(stopTime.timeInMillis)) {
|
||||
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||
ctx.getString(R.string.today) + " " + fmt.format(stopTime.time)
|
||||
} else {
|
||||
val fmt = DateFormat.getDateTimeInstance()
|
||||
fmt.format(stopTime.time)
|
||||
}
|
||||
if (startTime == GregorianCalendar(0, 0, 0)) {
|
||||
startTimeText = stopText
|
||||
durationText = "?"
|
||||
} else {
|
||||
if (startTime == null) {
|
||||
startTimeText = stopText
|
||||
durationText = ""
|
||||
} else {
|
||||
val startText = if (DateUtils.isToday(startTime.timeInMillis)) {
|
||||
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||
ctx.getString(R.string.today) + " " + fmt.format(startTime.time)
|
||||
} else {
|
||||
val fmt = DateFormat.getDateTimeInstance()
|
||||
fmt.format(startTime.time)
|
||||
}
|
||||
startTimeText = startText
|
||||
val duration = (stopTime.time.time - startTime.time.time) / 1000
|
||||
durationText = DateUtils.formatElapsedTime(duration)
|
||||
}
|
||||
}
|
||||
Text(text = startTimeText, color = LocalCustomColors.current.itemText)
|
||||
return durationText
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Duration(ctx: Context, detail: CallRow.Details, durationText: String) {
|
||||
val recording = detail.recording
|
||||
if (recording[0] != "") {
|
||||
Text(
|
||||
text = durationText,
|
||||
color = LocalCustomColors.current.accent,
|
||||
modifier = Modifier.padding(end = 12.dp)
|
||||
.clickable(onClick = {
|
||||
val builder = MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
|
||||
builder.setTitle("Playing recording ...")
|
||||
.setMessage("")
|
||||
.setCancelable(true)
|
||||
val dialog = builder.create()
|
||||
if (!decPlayer.isPlaying && !encPlayer.isPlaying) {
|
||||
decPlayer.reset()
|
||||
encPlayer.reset()
|
||||
Log.d(TAG, "Playing recordings ${recording[0]} and ${recording[1]}")
|
||||
decPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setOnPreparedListener {
|
||||
encPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setOnPreparedListener {
|
||||
it.start()
|
||||
decPlayer.start()
|
||||
Log.d(TAG, "Started players")
|
||||
dialog.show()
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Stopping encPlayer")
|
||||
it.stop()
|
||||
dialog.cancel()
|
||||
}
|
||||
try {
|
||||
val file = recording[0]
|
||||
val encFile = File(file)
|
||||
.copyTo(File(BaresipService.filesPath +
|
||||
"/tmp/encode.wav"), true)
|
||||
val encUri = encFile.toUri()
|
||||
setDataSource(ctx, encUri)
|
||||
prepareAsync()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "encPlayer IllegalArgumentException: $e")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "encPlayer IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "encPlayer Exception: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Stopping decPlayer")
|
||||
it.stop()
|
||||
}
|
||||
try {
|
||||
val file = recording[1]
|
||||
val decFile = File(file)
|
||||
.copyTo(File(BaresipService.filesPath +
|
||||
"/tmp/decode.wav"), true)
|
||||
val decUri = decFile.toUri()
|
||||
setDataSource(ctx, decUri)
|
||||
prepareAsync()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "decPlayer IllegalArgumentException: $e")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "decPlayer IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "decPlayer Exception: $e")
|
||||
}
|
||||
}
|
||||
} else if (decPlayer.isPlaying && encPlayer.isPlaying) {
|
||||
decPlayer.stop()
|
||||
encPlayer.stop()
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
Text(text = durationText,
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
color = LocalCustomColors.current.itemText)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
MainActivity.activityAor = aor
|
||||
super.onPause()
|
||||
|
||||
@ -1,166 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.text.format.DateUtils
|
||||
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 androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.DateFormat
|
||||
|
||||
import java.util.*
|
||||
|
||||
class CallDetailsAdapter(private val ctx: Context, private val rows: ArrayList<CallRow.Details>,
|
||||
private val decPlayer: MediaPlayer, private val encPlayer: MediaPlayer) :
|
||||
ArrayAdapter<CallRow.Details>(ctx, R.layout.call_detail_row, rows) {
|
||||
|
||||
private val layoutInflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
private class ViewHolder(view: View?) {
|
||||
val directionView: ImageView = view?.findViewById(R.id.direction)!!
|
||||
val timeView: TextView = view?.findViewById(R.id.time)!!
|
||||
val durationView: TextView = view?.findViewById(R.id.duration)!!
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
|
||||
val viewHolder: ViewHolder
|
||||
val rowView: View
|
||||
|
||||
if (view == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.call_detail_row, parent, false)
|
||||
viewHolder = ViewHolder(rowView)
|
||||
rowView.tag = viewHolder
|
||||
} else {
|
||||
rowView = view
|
||||
viewHolder = rowView.tag as ViewHolder
|
||||
}
|
||||
|
||||
val direction = rows[position].direction
|
||||
viewHolder.directionView.setImageResource(direction)
|
||||
|
||||
val startTime = rows[position].startTime
|
||||
val stopTime = rows[position].stopTime
|
||||
|
||||
val stopText = if (DateUtils.isToday(stopTime.timeInMillis)) {
|
||||
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||
ctx.getString(R.string.today) + " " + fmt.format(stopTime.time)
|
||||
} else {
|
||||
val fmt = DateFormat.getDateTimeInstance()
|
||||
fmt.format(stopTime.time)
|
||||
}
|
||||
if (startTime == GregorianCalendar(0, 0, 0)) {
|
||||
viewHolder.timeView.text = stopText
|
||||
viewHolder.durationView.text = "?"
|
||||
} else {
|
||||
if (startTime == null) {
|
||||
viewHolder.timeView.text = stopText
|
||||
viewHolder.durationView.text = ""
|
||||
} else {
|
||||
val startText = if (DateUtils.isToday(startTime.timeInMillis)) {
|
||||
val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||
ctx.getString(R.string.today) + " " + fmt.format(startTime.time)
|
||||
} else {
|
||||
val fmt = DateFormat.getDateTimeInstance()
|
||||
fmt.format(startTime.time)
|
||||
}
|
||||
viewHolder.timeView.text = startText
|
||||
val duration = (stopTime.time.time - startTime.time.time) / 1000
|
||||
viewHolder.durationView.text = DateUtils.formatElapsedTime(duration)
|
||||
val recording = rows[position].recording
|
||||
if (recording[0] != "") {
|
||||
viewHolder.durationView.setTextColor(ContextCompat.getColor(context, R.color.colorAccent))
|
||||
viewHolder.durationView.setOnClickListener {
|
||||
if (!decPlayer.isPlaying && !encPlayer.isPlaying) {
|
||||
decPlayer.reset()
|
||||
encPlayer.reset()
|
||||
playRecording(ctx, viewHolder.durationView, recording)
|
||||
} else if (decPlayer.isPlaying && encPlayer.isPlaying) {
|
||||
decPlayer.stop()
|
||||
encPlayer.stop()
|
||||
viewHolder.durationView.setTextColor(ContextCompat.getColor(ctx, R.color.colorItemText))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rowView
|
||||
}
|
||||
|
||||
private fun playRecording(ctx: Context, textView: TextView, recording: Array<String>) {
|
||||
Log.d(TAG, "Playing recordings ${recording[0]} and ${recording[1]}")
|
||||
decPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setOnPreparedListener {
|
||||
encPlayer.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.build()
|
||||
)
|
||||
setOnPreparedListener {
|
||||
it.start()
|
||||
decPlayer.start()
|
||||
textView.setTextColor(ContextCompat.getColor(ctx, R.color.colorAccent))
|
||||
Log.d(TAG, "Started players")
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Stopping encPlayer")
|
||||
it.stop()
|
||||
textView.setTextColor(ContextCompat.getColor(ctx, R.color.colorItemText))
|
||||
}
|
||||
try {
|
||||
val file = recording[0]
|
||||
val encFile = File(file).copyTo(File(BaresipService.filesPath + "/tmp/encode.wav"), true)
|
||||
val encUri = encFile.toUri()
|
||||
setDataSource(ctx, encUri)
|
||||
prepareAsync()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "encPlayer IllegalArgumentException: $e")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "encPlayer IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "encPlayer Exception: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
setOnCompletionListener {
|
||||
Log.d(TAG, "Stopping decPlayer")
|
||||
it.stop()
|
||||
}
|
||||
try {
|
||||
val file = recording[1]
|
||||
val decFile = File(file).copyTo(File(BaresipService.filesPath + "/tmp/decode.wav"), true)
|
||||
val decUri = decFile.toUri()
|
||||
setDataSource(ctx, decUri)
|
||||
prepareAsync()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "decPlayer IllegalArgumentException: $e")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "decPlayer IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "decPlayer Exception: $e")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
class CallListAdapter(private val ctx: Context, private val account: Account,
|
||||
private val rows: ArrayList<CallRow>) :
|
||||
ArrayAdapter<CallRow>(ctx, R.layout.call_row, rows) {
|
||||
|
||||
private val layoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private class ViewHolder(view: View?) {
|
||||
val textAvatarView: TextView = view?.findViewById(R.id.TextAvatar)!!
|
||||
val imageAvatarView: ImageView = view?.findViewById(R.id.ImageAvatar)!!
|
||||
val directionsView: LinearLayout = view?.findViewById(R.id.directions)!!
|
||||
val etcView: TextView = view?.findViewById(R.id.etc)!!
|
||||
val peerURIView: TextView = view?.findViewById(R.id.peer_uri)!!
|
||||
val timeView: TextView = view?.findViewById(R.id.time)!!
|
||||
}
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
|
||||
val viewHolder: ViewHolder
|
||||
val rowView: View
|
||||
|
||||
if (view == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.call_row, parent, false)
|
||||
viewHolder = ViewHolder(rowView)
|
||||
rowView.tag = viewHolder
|
||||
} else {
|
||||
rowView = view
|
||||
viewHolder = rowView.tag as ViewHolder
|
||||
}
|
||||
|
||||
val callRow = rows[position]
|
||||
|
||||
Utils.setAvatar(ctx, viewHolder.imageAvatarView, viewHolder.textAvatarView, callRow.peerUri)
|
||||
|
||||
viewHolder.directionsView.removeAllViews()
|
||||
var count = 1
|
||||
for (d in callRow.details) {
|
||||
if (d.recording[0] != "")
|
||||
viewHolder.timeView.setTextColor(ContextCompat.getColor(context, R.color.colorAccent))
|
||||
if (count > 3) {
|
||||
viewHolder.etcView.text = "..."
|
||||
continue
|
||||
}
|
||||
val dirView = ImageView(ctx)
|
||||
val params = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
dirView.layoutParams = params
|
||||
dirView.setPadding(0, 5, 0, 0)
|
||||
dirView.setImageResource(d.direction)
|
||||
viewHolder.directionsView.addView(dirView)
|
||||
count++
|
||||
}
|
||||
if (count <= 3)
|
||||
viewHolder.etcView.text = ""
|
||||
|
||||
viewHolder.peerURIView.text = Utils.friendlyUri(ctx, callRow.peerUri, account)
|
||||
viewHolder.timeView.text = Utils.relativeTime(ctx, callRow.stopTime)
|
||||
|
||||
viewHolder.timeView.setOnClickListener {
|
||||
val i = Intent(ctx, CallDetailsActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", account.aor)
|
||||
b.putString("peer", viewHolder.peerURIView.text!!.toString())
|
||||
b.putInt("position", position)
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i)
|
||||
}
|
||||
|
||||
return rowView
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,31 +1,75 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tutpro.baresip.databinding.ActivityCallsBinding
|
||||
import com.tutpro.baresip.Contact.BaresipContact
|
||||
import com.tutpro.baresip.CustomElements.ImageAvatar
|
||||
import com.tutpro.baresip.CustomElements.TextAvatar
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
class CallsActivity : AppCompatActivity() {
|
||||
class CallsActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var binding: ActivityCallsBinding
|
||||
private lateinit var account: Account
|
||||
private lateinit var clAdapter: CallListAdapter
|
||||
|
||||
private var aor = ""
|
||||
private var lastClick: Long = 0
|
||||
@ -40,153 +84,402 @@ class CallsActivity : AppCompatActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityCallsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
enableEdgeToEdge()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
if (Build.VERSION.SDK_INT >= 35)
|
||||
binding.CallsView.updatePadding(top = systemBars.top + 56)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
if (!Utils.isDarkTheme(this))
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
|
||||
val title = String.format(getString(R.string.call_history))
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
Utils.addActivity("calls,$aor")
|
||||
|
||||
val ua = UserAgent.ofAor(aor)!!
|
||||
account = ua.account
|
||||
|
||||
val headerView = binding.account
|
||||
val headerText = getString(R.string.account) + " " +
|
||||
if (account.nickName != "")
|
||||
account.nickName
|
||||
else
|
||||
aor.split(":")[1]
|
||||
headerView.text = headerText
|
||||
|
||||
val listView = binding.calls
|
||||
aorGenerateHistory(aor)
|
||||
clAdapter = CallListAdapter(this, account, uaHistory)
|
||||
listView.adapter = clAdapter
|
||||
listView.isLongClickable = true
|
||||
|
||||
listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ ->
|
||||
val peerUri = uaHistory[pos].peerUri
|
||||
val peerName = Utils.friendlyUri(this, peerUri, account)
|
||||
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> {
|
||||
BaresipService.activities.remove("calls,$aor")
|
||||
MainActivity.activityAor = aor
|
||||
returnResult()
|
||||
val i = Intent(this@CallsActivity, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
if (which == DialogInterface.BUTTON_NEGATIVE)
|
||||
i.putExtra("action", "call")
|
||||
else
|
||||
i.putExtra("action", "message")
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", peerUri)
|
||||
startActivity(i)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
with(MaterialAlertDialogBuilder(this@CallsActivity, R.style.AlertDialogTheme)) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.calls_call_message_question),
|
||||
peerName
|
||||
)
|
||||
)
|
||||
setNeutralButton(getString(R.string.cancel), dialogClickListener)
|
||||
setNegativeButton(getString(R.string.call), dialogClickListener)
|
||||
setPositiveButton(getString(R.string.send_message), dialogClickListener)
|
||||
show()
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
CallsScreen(this, title) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val contactRequest =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
clAdapter.notifyDataSetChanged()
|
||||
account.missedCalls = false
|
||||
invalidateOptionsMenu()
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = { TopAppBar(title, navigateBack) },
|
||||
content = { contentPadding ->
|
||||
CallsContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopAppBar(title: String, navigateBack: () -> Unit) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val delete = String.format(getString(R.string.delete))
|
||||
val disable = String.format(getString(R.string.disable_history))
|
||||
val enable = String.format(getString(R.string.enable_history))
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { expanded = !expanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
CustomElements.DropdownMenu(expanded,
|
||||
{ expanded = false },
|
||||
listOf(delete, if (account.callHistory) disable else enable),
|
||||
onItemClick = { selectedItem ->
|
||||
expanded = false
|
||||
when (selectedItem) {
|
||||
delete ->
|
||||
with(
|
||||
MaterialAlertDialogBuilder(
|
||||
this@CallsActivity,
|
||||
R.style.AlertDialogTheme
|
||||
)
|
||||
) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.delete_history_alert),
|
||||
aor.substringAfter(":")
|
||||
)
|
||||
)
|
||||
setPositiveButton(delete) { dialog, _ ->
|
||||
CallHistoryNew.clear(aor)
|
||||
CallHistoryNew.save()
|
||||
aorGenerateHistory(aor)
|
||||
//clAdapter.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNeutralButton(getText(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
show()
|
||||
}
|
||||
|
||||
disable, enable -> {
|
||||
account.callHistory = !account.callHistory
|
||||
AccountsActivity.saveAccounts()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Account(account)
|
||||
Calls(ctx, account)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Account(account: Account) {
|
||||
val headerText = getString(R.string.account) + " " +
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
aor.split(":")[1]
|
||||
Text(
|
||||
text = headerText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Calls(ctx: Context, account: Account) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
)
|
||||
.background(LocalCustomColors.current.background),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(items = uaHistory.value) { callRow ->
|
||||
|
||||
var recordings = false
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.combinedClickable(
|
||||
onClick = {
|
||||
callMessageClick(ctx, callRow)
|
||||
},
|
||||
onLongClick = {
|
||||
callMessageLongClick(ctx, callRow)
|
||||
}
|
||||
)
|
||||
) {
|
||||
val uri = callRow.peerUri
|
||||
when (val contact = Contact.findContact(uri)) {
|
||||
is Contact.BaresipContact -> {
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
ImageAvatar(avatarImage)
|
||||
else
|
||||
TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
is Contact.AndroidContact -> {
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null)
|
||||
AsyncImage(
|
||||
model = thumbNailUri,
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(36.dp).clip(CircleShape),
|
||||
)
|
||||
else
|
||||
TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
null -> {
|
||||
val avatarImage = BitmapFactory
|
||||
.decodeResource(ctx.resources, R.drawable.person_image)
|
||||
ImageAvatar(avatarImage)
|
||||
}
|
||||
}
|
||||
var count = 1
|
||||
for (d in callRow.details) {
|
||||
if (d.recording[0] != "")
|
||||
recordings = true
|
||||
if (count > 3)
|
||||
continue
|
||||
Image(painterResource(d.direction), "Direction")
|
||||
count++
|
||||
}
|
||||
if (count > 3)
|
||||
Text("...", color = LocalCustomColors.current.itemText)
|
||||
Text(text = Utils.friendlyUri(ctx, callRow.peerUri, account),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalCustomColors.current.itemText
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(modifier = Modifier.width(56.dp)) {
|
||||
Text(
|
||||
text = Utils.relativeTime(ctx, callRow.stopTime),
|
||||
fontSize = 12.sp,
|
||||
minLines = 2, maxLines = 2,
|
||||
lineHeight = 16.sp,
|
||||
textAlign = TextAlign.End,
|
||||
color = if (recordings)
|
||||
LocalCustomColors.current.accent
|
||||
else
|
||||
LocalCustomColors.current.itemText,
|
||||
modifier = Modifier.padding(end = 16.dp).width(64.dp)
|
||||
.clickable(onClick = {
|
||||
val i = Intent(ctx, CallDetailsActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", account.aor)
|
||||
b.putString("peer", callRow.peerUri)
|
||||
b.putInt("position", uaHistory.value.indexOf(callRow))
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listView.onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, pos, _ ->
|
||||
val peerUri = uaHistory[pos].peerUri
|
||||
val peerName = Utils.friendlyUri(this, peerUri, account)
|
||||
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
|
||||
private fun callMessageClick(ctx: Context, callRow: CallRow) {
|
||||
val peerUri = callRow.peerUri
|
||||
val peerName = Utils.friendlyUri(ctx, peerUri, account)
|
||||
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> {
|
||||
BaresipService.activities.remove("calls,$aor")
|
||||
MainActivity.activityAor = aor
|
||||
returnResult()
|
||||
val i = Intent(this@CallsActivity, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
if (which == DialogInterface.BUTTON_NEGATIVE)
|
||||
i.putExtra("action", "call")
|
||||
else
|
||||
i.putExtra("action", "message")
|
||||
i.putExtra("uap", UserAgent.ofAor(aor)!!.uap)
|
||||
i.putExtra("peer", peerUri)
|
||||
startActivity(i)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
with(
|
||||
MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
|
||||
) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(ctx.getString(R.string.contact_action_question), peerName)
|
||||
)
|
||||
setNeutralButton(
|
||||
ctx.getText(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setNegativeButton(
|
||||
ctx.getText(R.string.call),
|
||||
dialogClickListener
|
||||
)
|
||||
setPositiveButton(
|
||||
ctx.getText(R.string.send_message),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun callMessageLongClick(ctx: Context, callRow: CallRow) {
|
||||
val peerUri = callRow.peerUri
|
||||
val peerName = Utils.friendlyUri(ctx, peerUri, account)
|
||||
val dialogClickListener =
|
||||
DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
val i = Intent(this, ContactActivity::class.java)
|
||||
val i = Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", peerUri)
|
||||
i.putExtras(b)
|
||||
contactRequest.launch(i)
|
||||
ctx.startActivity(i)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
removeUaHistoryAt(pos)
|
||||
removeFromHistory(callRow)
|
||||
CallHistoryNew.save()
|
||||
clAdapter.notifyDataSetChanged()
|
||||
/// clAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
val callText: String = if (uaHistory[pos].details.size > 1)
|
||||
getString(R.string.calls_calls)
|
||||
else
|
||||
getString(R.string.calls_call)
|
||||
val builder = MaterialAlertDialogBuilder(this@CallsActivity, R.style.AlertDialogTheme)
|
||||
if (!Contact.nameExists(peerName, false))
|
||||
with(builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.calls_add_delete_question),
|
||||
peerName, callText
|
||||
)
|
||||
val callText: String = if (callRow.details.size > 1)
|
||||
getString(R.string.calls_calls)
|
||||
else
|
||||
getString(R.string.calls_call)
|
||||
val builder = MaterialAlertDialogBuilder(
|
||||
this@CallsActivity,
|
||||
R.style.AlertDialogTheme
|
||||
)
|
||||
if (!Contact.nameExists(peerName, BaresipService.contacts, false))
|
||||
with(builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.calls_add_delete_question),
|
||||
peerName, callText
|
||||
)
|
||||
setNeutralButton(getString(R.string.cancel), dialogClickListener)
|
||||
setPositiveButton(getString(R.string.delete), dialogClickListener)
|
||||
setNegativeButton(getString(R.string.add_contact), dialogClickListener)
|
||||
show()
|
||||
}
|
||||
else
|
||||
with(builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.calls_delete_question),
|
||||
peerName, callText
|
||||
)
|
||||
)
|
||||
setNeutralButton(
|
||||
getString(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setPositiveButton(
|
||||
getString(R.string.delete),
|
||||
dialogClickListener
|
||||
)
|
||||
setNegativeButton(
|
||||
getString(R.string.add_contact),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
else
|
||||
with(builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.calls_delete_question),
|
||||
peerName, callText
|
||||
)
|
||||
setNeutralButton(getString(R.string.cancel), dialogClickListener)
|
||||
setPositiveButton(getString(R.string.delete), dialogClickListener)
|
||||
show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
ua.account.missedCalls = false
|
||||
invalidateOptionsMenu()
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
)
|
||||
setNeutralButton(
|
||||
getString(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setPositiveButton(
|
||||
getString(R.string.delete),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@ -205,7 +498,7 @@ class CallsActivity : AppCompatActivity() {
|
||||
CallHistoryNew.clear(aor)
|
||||
CallHistoryNew.save()
|
||||
aorGenerateHistory(aor)
|
||||
clAdapter.notifyDataSetChanged()
|
||||
// clAdapter.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNeutralButton(getText(R.string.cancel)) { dialog, _ ->
|
||||
@ -243,7 +536,7 @@ class CallsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun returnResult() {
|
||||
setResult(Activity.RESULT_CANCELED, Intent())
|
||||
setResult(RESULT_CANCELED, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
@ -263,7 +556,7 @@ class CallsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun aorGenerateHistory(aor: String) {
|
||||
uaHistory.clear()
|
||||
uaHistory.value = emptyList()
|
||||
for (i in BaresipService.callHistory.indices.reversed()) {
|
||||
val h = BaresipService.callHistory[i]
|
||||
if (h.aor == aor) {
|
||||
@ -286,34 +579,54 @@ class CallsActivity : AppCompatActivity() {
|
||||
R.drawable.call_missed_out
|
||||
}
|
||||
}
|
||||
if (uaHistory.isNotEmpty() && (uaHistory.last().peerUri == h.peerUri))
|
||||
uaHistory.last().details.add(CallRow.Details(
|
||||
if (uaHistory.value.isNotEmpty() && (uaHistory.value.last().peerUri == h.peerUri))
|
||||
uaHistory.value.last().details.add(CallRow.Details(
|
||||
direction, h.startTime,
|
||||
h.stopTime, h.recording
|
||||
))
|
||||
else
|
||||
uaHistory.add(CallRow(
|
||||
h.aor, h.peerUri, direction, h.startTime, h.stopTime,
|
||||
h.recording
|
||||
))
|
||||
addToUaHistory(
|
||||
CallRow(h.aor, h.peerUri, direction, h.startTime, h.stopTime, h.recording)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeUaHistoryAt(i: Int) {
|
||||
for (details in uaHistory[i].details) {
|
||||
private fun removeFromHistory(callRow: CallRow) {
|
||||
for (details in callRow.details) {
|
||||
if (details.recording[0] != "")
|
||||
CallHistoryNew.deleteRecording(details.recording)
|
||||
BaresipService.callHistory.removeAll {
|
||||
it.startTime == details.startTime && it.stopTime == details.stopTime
|
||||
}
|
||||
}
|
||||
CallHistoryNew.deleteRecording(uaHistory[i].recording)
|
||||
uaHistory.removeAt(i)
|
||||
CallHistoryNew.deleteRecording(callRow.recording)
|
||||
deleteFromUaHistory(callRow)
|
||||
updateUaHistory()
|
||||
}
|
||||
|
||||
private fun addToUaHistory(callRow: CallRow) {
|
||||
val updatedList = uaHistory.value.toMutableList()
|
||||
updatedList.add(callRow)
|
||||
uaHistory.value = updatedList
|
||||
}
|
||||
|
||||
private fun deleteFromUaHistory(callRow: CallRow) {
|
||||
val updatedList = uaHistory.value.toMutableList()
|
||||
updatedList.remove(callRow)
|
||||
uaHistory.value = updatedList
|
||||
}
|
||||
|
||||
private fun updateUaHistory() {
|
||||
val updatedList: MutableList<CallRow> = mutableListOf()
|
||||
for (h in uaHistory.value)
|
||||
updatedList.add(h)
|
||||
uaHistory.value = updatedList
|
||||
}
|
||||
|
||||
@SuppressLint("MutableCollectionMutableState")
|
||||
companion object {
|
||||
var uaHistory = ArrayList<CallRow>()
|
||||
val uaHistory = mutableStateOf(emptyList<CallRow>())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,45 +1,103 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.text.format.DateUtils.isToday
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ListView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Observer
|
||||
import com.tutpro.baresip.databinding.ActivityChatBinding
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tutpro.baresip.CustomElements.Text
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DateFormat
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
class ChatActivity : AppCompatActivity() {
|
||||
class ChatActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var binding: ActivityChatBinding
|
||||
private lateinit var chatMessages: ArrayList<Message>
|
||||
private lateinit var mlAdapter: MessageListAdapter
|
||||
private lateinit var listView: ListView
|
||||
private lateinit var newMessage: EditText
|
||||
private lateinit var sendButton: ImageButton
|
||||
private lateinit var imm: InputMethodManager
|
||||
private lateinit var aor: String
|
||||
private lateinit var account: Account
|
||||
private lateinit var peerUri: String
|
||||
private lateinit var chatPeer: String
|
||||
private lateinit var ua: UserAgent
|
||||
|
||||
private var _chatMessages = mutableStateOf<List<Message>>(emptyList())
|
||||
private var chatMessages : List<Message> by _chatMessages
|
||||
private var focus = false
|
||||
private var lastCall: Long = 0
|
||||
private var unsentMessage = ""
|
||||
private var keyboardController: SoftwareKeyboardController? = null
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
@ -51,28 +109,15 @@ class ChatActivity : AppCompatActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityChatBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
if (Build.VERSION.SDK_INT >= 35)
|
||||
binding.ChatView.updatePadding(top = systemBars.top + 56)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
if (!Utils.isDarkTheme(this))
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
enableEdgeToEdge()
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
peerUri = intent.getStringExtra("peer")!!
|
||||
account = UserAgent.ofAor(aor)!!.account
|
||||
focus = intent.getBooleanExtra("focus", false)
|
||||
|
||||
if (BaresipService.activities.first().startsWith("chat,$aor,$peerUri")) {
|
||||
returnResult(Activity.RESULT_CANCELED)
|
||||
returnResult(RESULT_CANCELED)
|
||||
return
|
||||
} else {
|
||||
Utils.addActivity("chat,$aor,$peerUri,$focus")
|
||||
@ -82,92 +127,474 @@ class ChatActivity : AppCompatActivity() {
|
||||
if (userAgent == null) {
|
||||
Log.w(TAG, "MessageActivity did not find ua of $aor")
|
||||
MainActivity.activityAor = aor
|
||||
returnResult(Activity.RESULT_CANCELED)
|
||||
returnResult(RESULT_CANCELED)
|
||||
return
|
||||
} else {
|
||||
ua = userAgent
|
||||
}
|
||||
|
||||
chatPeer = Utils.friendlyUri(this, peerUri, userAgent.account, true)
|
||||
|
||||
imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
this@ChatActivity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
|
||||
|
||||
val chatPeer = Utils.friendlyUri(this, peerUri, userAgent.account, true)
|
||||
|
||||
title = String.format(getString(R.string.chat_with), chatPeer)
|
||||
|
||||
val headerView = binding.account
|
||||
val headerText = getString(R.string.account) + " " +
|
||||
if (ua.account.nickName != "")
|
||||
ua.account.nickName
|
||||
else
|
||||
aor.split(":")[1]
|
||||
headerView.text = headerText
|
||||
|
||||
listView = binding.messages
|
||||
|
||||
chatMessages = uaPeerMessages(aor, peerUri)
|
||||
mlAdapter = MessageListAdapter(this, peerUri, chatPeer, chatMessages)
|
||||
val title = String.format(getString(R.string.chat_with), chatPeer)
|
||||
|
||||
val messagesObserver = Observer<Long> {
|
||||
chatMessages.clear()
|
||||
chatMessages.addAll(uaPeerMessages(aor, peerUri))
|
||||
mlAdapter.notifyDataSetChanged()
|
||||
_chatMessages.value = listOf()
|
||||
_chatMessages.value = uaPeerMessages(aor, peerUri)
|
||||
}
|
||||
BaresipService.messageUpdate.observe(this, messagesObserver)
|
||||
|
||||
listView.adapter = mlAdapter
|
||||
//listView.isLongClickable = true
|
||||
val footerView = View(applicationContext)
|
||||
listView.addFooterView(footerView)
|
||||
listView.smoothScrollToPosition(mlAdapter.count - 1)
|
||||
|
||||
newMessage = binding.text
|
||||
newMessage.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
}
|
||||
}
|
||||
|
||||
if (focus) newMessage.requestFocus()
|
||||
|
||||
sendButton = binding.sendButton
|
||||
sendButton.setOnClickListener {
|
||||
val msgText = newMessage.text.toString()
|
||||
if (msgText.isNotEmpty()) {
|
||||
imm.hideSoftInputFromWindow(newMessage.windowToken, 0)
|
||||
val time = System.currentTimeMillis()
|
||||
val msg = Message(aor, peerUri, msgText, time, MESSAGE_UP_WAIT, 0, "", true)
|
||||
msg.add()
|
||||
var msgUri = ""
|
||||
chatMessages.add(msg)
|
||||
if (Utils.isTelUri(peerUri))
|
||||
if (ua.account.telProvider == "") {
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.no_telephony_provider),
|
||||
Utils.plainAor(aor)))
|
||||
} else {
|
||||
msgUri = Utils.telToSip(peerUri, ua.account)
|
||||
}
|
||||
else
|
||||
msgUri = peerUri
|
||||
if (msgUri != "")
|
||||
if (Api.message_send(ua.uap, msgUri, msgText, time.toString()) != 0) {
|
||||
Toast.makeText(applicationContext, "${getString(R.string.message_failed)}!",
|
||||
Toast.LENGTH_SHORT).show()
|
||||
msg.direction = MESSAGE_UP_FAIL
|
||||
msg.responseReason = getString(R.string.message_failed)
|
||||
} else {
|
||||
newMessage.setText("")
|
||||
BaresipService.chatTexts.remove("$aor::$peerUri")
|
||||
}
|
||||
mlAdapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
ua.account.unreadMessages = false
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
setContent {
|
||||
AppTheme {
|
||||
keyboardController = LocalSoftwareKeyboardController.current
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
ChatScreen(this, title) { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = { TopAppBar(ctx, title, navigateBack) },
|
||||
bottomBar = { NewMessage(ctx, peerUri) },
|
||||
content = { contentPadding ->
|
||||
ChatContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopAppBar(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastCall > 1000) {
|
||||
lastCall = SystemClock.elapsedRealtime()
|
||||
val intent = Intent(ctx, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
intent.putExtra("action", "call")
|
||||
intent.putExtra("uap", ua.uap)
|
||||
intent.putExtra("peer", peerUri)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.call_small),
|
||||
contentDescription = "Call",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
Account(account)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Messages(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Account(account: Account) {
|
||||
val headerText = getString(R.string.account) + " " +
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
aor.split(":")[1]
|
||||
Text(
|
||||
text = headerText, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Messages(ctx: Context) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
_chatMessages.value = uaPeerMessages(aor, peerUri)
|
||||
}
|
||||
|
||||
LaunchedEffect(chatMessages) {
|
||||
// Scroll to the bottom when new messages are added
|
||||
if (chatMessages.isNotEmpty()) {
|
||||
coroutineScope.launch {
|
||||
lazyListState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 2.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
)
|
||||
.background(LocalCustomColors.current.background),
|
||||
reverseLayout = true,
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(items = chatMessages, key = { message -> message.timeStamp }) { message ->
|
||||
val down = message.direction == MESSAGE_DOWN
|
||||
val peer: String = if (down) {
|
||||
if (chatPeer.startsWith("sip:") &&
|
||||
(Utils.uriHostPart(message.peerUri) == Utils.uriHostPart(message.aor)))
|
||||
Utils.uriUserPart(message.peerUri)
|
||||
else
|
||||
chatPeer
|
||||
}
|
||||
else
|
||||
ctx.getString(R.string.you)
|
||||
var info: String
|
||||
val cal = GregorianCalendar()
|
||||
cal.timeInMillis = message.timeStamp
|
||||
val fmt: DateFormat = if (isToday(message.timeStamp))
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
else
|
||||
DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
info = fmt.format(cal.time)
|
||||
if (info.length < 6) info = "${ctx.getString(R.string.today)} $info"
|
||||
if (message.direction == MESSAGE_UP_FAIL) {
|
||||
info = if (message.responseCode != 0)
|
||||
"$info - ${ctx.getString(R.string.message_failed)}: " + "${message.responseCode} ${message.responseReason}"
|
||||
else
|
||||
"$info - ${ctx.getString(R.string.sending_failed)}"
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(end = 12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
val dialogClickListener =
|
||||
DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
val i = Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", peerUri)
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
message.delete()
|
||||
_chatMessages.value = uaPeerMessages(aor, peerUri)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
val builder = MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
|
||||
if (chatPeer == peerUri)
|
||||
with(builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
ctx.getString(R.string.long_message_question),
|
||||
peerUri
|
||||
)
|
||||
)
|
||||
setNeutralButton(
|
||||
ctx.getString(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setNegativeButton(
|
||||
ctx.getString(R.string.delete),
|
||||
dialogClickListener
|
||||
)
|
||||
setPositiveButton(
|
||||
ctx.getString(R.string.add_contact),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
else
|
||||
with(builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(ctx.getText(R.string.short_message_question))
|
||||
setNeutralButton(
|
||||
ctx.getString(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setNegativeButton(
|
||||
ctx.getString(R.string.delete),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
},
|
||||
shape = if (message.direction == MESSAGE_DOWN)
|
||||
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
|
||||
else
|
||||
RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor =
|
||||
if (message.direction == MESSAGE_DOWN)
|
||||
LocalCustomColors.current.secondaryLight
|
||||
else
|
||||
LocalCustomColors.current.primaryLight),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
) {
|
||||
Column {
|
||||
Row {
|
||||
Text(
|
||||
peer, color = LocalCustomColors.current.dark,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
info, color = LocalCustomColors.current.dark,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Row {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = message.message,
|
||||
color = LocalCustomColors.current.dark,
|
||||
fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
|
||||
FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NewMessage(ctx: Context, peerUri: String) {
|
||||
var newMessage by remember { mutableStateOf("") }
|
||||
// var textFieldState = rememberTextFieldState()
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
OutlinedTextField(
|
||||
value = newMessage,
|
||||
placeholder = { Text(text = getString(R.string.new_message)) },
|
||||
onValueChange = { newMessage = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
val msgText = newMessage
|
||||
if (msgText.isNotEmpty()) {
|
||||
keyboardController?.hide()
|
||||
val time = System.currentTimeMillis()
|
||||
val msg = Message(
|
||||
aor,
|
||||
peerUri,
|
||||
msgText,
|
||||
time,
|
||||
MESSAGE_UP_WAIT,
|
||||
0,
|
||||
"",
|
||||
true
|
||||
)
|
||||
msg.add()
|
||||
var msgUri = ""
|
||||
_chatMessages.value += msg
|
||||
if (Utils.isTelUri(peerUri))
|
||||
if (ua.account.telProvider == "") {
|
||||
Utils.alertView(
|
||||
ctx, getString(R.string.notice),
|
||||
String.format(
|
||||
getString(R.string.no_telephony_provider),
|
||||
Utils.plainAor(aor)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
msgUri = Utils.telToSip(peerUri, ua.account)
|
||||
}
|
||||
else
|
||||
msgUri = peerUri
|
||||
if (msgUri != "")
|
||||
if (Api.message_send(
|
||||
ua.uap,
|
||||
msgUri,
|
||||
msgText,
|
||||
time.toString()
|
||||
) != 0
|
||||
) {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
"${getString(R.string.message_failed)}!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
msg.direction = MESSAGE_UP_FAIL
|
||||
msg.responseReason = getString(R.string.message_failed)
|
||||
} else {
|
||||
newMessage = ""
|
||||
unsentMessage = ""
|
||||
keyboardController?.hide()
|
||||
BaresipService.chatTexts.remove("$aor::$peerUri")
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
val clipboardManager =
|
||||
ctx.getSystemService(Context.CLIPBOARD_SERVICE) as
|
||||
android.content.ClipboardManager
|
||||
val clipData = clipboardManager.primaryClip
|
||||
if (clipData != null && clipData.itemCount > 0) {
|
||||
val pastedText = clipData.getItemAt(0).text.toString()
|
||||
newMessage = pastedText
|
||||
} else {
|
||||
Toast.makeText(ctx, "Nothing to paste", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
),
|
||||
singleLine = false,
|
||||
trailingIcon = {
|
||||
if (newMessage.isNotEmpty()) {
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = "Clear",
|
||||
modifier = Modifier.clickable { newMessage = "" }
|
||||
)
|
||||
} },
|
||||
label = { Text(text = getString(R.string.new_message), fontSize = 18.sp) },
|
||||
textStyle = TextStyle(fontSize = 18.sp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
keyboardType = KeyboardType.Text,
|
||||
autoCorrectEnabled = true
|
||||
)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.send),
|
||||
contentDescription = "Send",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(36.dp)
|
||||
.clickable {
|
||||
val msgText = newMessage
|
||||
if (msgText.isNotEmpty()) {
|
||||
keyboardController?.hide()
|
||||
val time = System.currentTimeMillis()
|
||||
val msg = Message(
|
||||
aor,
|
||||
peerUri,
|
||||
msgText,
|
||||
time,
|
||||
MESSAGE_UP_WAIT,
|
||||
0,
|
||||
"",
|
||||
true
|
||||
)
|
||||
msg.add()
|
||||
var msgUri = ""
|
||||
_chatMessages.value += msg
|
||||
if (Utils.isTelUri(peerUri))
|
||||
if (ua.account.telProvider == "") {
|
||||
Utils.alertView(
|
||||
ctx, getString(R.string.notice),
|
||||
String.format(
|
||||
getString(R.string.no_telephony_provider),
|
||||
Utils.plainAor(aor)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
msgUri = Utils.telToSip(peerUri, ua.account)
|
||||
}
|
||||
else
|
||||
msgUri = peerUri
|
||||
if (msgUri != "")
|
||||
if (Api.message_send(
|
||||
ua.uap,
|
||||
msgUri,
|
||||
msgText,
|
||||
time.toString()
|
||||
) != 0
|
||||
) {
|
||||
Toast.makeText(
|
||||
ctx,
|
||||
"${getString(R.string.message_failed)}!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
msg.direction = MESSAGE_UP_FAIL
|
||||
msg.responseReason = getString(R.string.message_failed)
|
||||
} else {
|
||||
newMessage = ""
|
||||
unsentMessage = ""
|
||||
keyboardController?.hide()
|
||||
BaresipService.chatTexts.remove("$aor::$peerUri")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@ -179,61 +606,24 @@ class ChatActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
val chatText = BaresipService.chatTexts["$aor::$peerUri"]
|
||||
if (chatText != null) {
|
||||
Log.d(TAG, "Restoring newMessage ${newMessage.text} for $aor::$peerUri")
|
||||
newMessage.setText(chatText)
|
||||
newMessage.requestFocus()
|
||||
Log.d(TAG, "Restoring newMessage $chatText for $aor::$peerUri")
|
||||
unsentMessage = chatText
|
||||
//newMessage.requestFocus()
|
||||
BaresipService.chatTexts.remove("$aor::$peerUri")
|
||||
}
|
||||
chatMessages.clear()
|
||||
chatMessages.addAll(uaPeerMessages(aor, peerUri))
|
||||
mlAdapter.notifyDataSetChanged()
|
||||
_chatMessages.value = uaPeerMessages(aor, peerUri)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (newMessage.text.toString() != "") {
|
||||
Log.d(TAG, "Saving newMessage ${newMessage.text} for $aor::$peerUri")
|
||||
BaresipService.chatTexts["$aor::$peerUri"] = newMessage.text.toString()
|
||||
if (unsentMessage != "") {
|
||||
Log.d(TAG, "Saving newMessage $unsentMessage for $aor::$peerUri")
|
||||
BaresipService.chatTexts["$aor::$peerUri"] = unsentMessage
|
||||
}
|
||||
MainActivity.activityAor = aor
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
if ((BaresipService.activities.indexOf("chat,$aor,$peerUri,false") == -1) &&
|
||||
(BaresipService.activities.indexOf("chat,$aor,$peerUri,true") == -1))
|
||||
return true
|
||||
|
||||
when (item.itemId) {
|
||||
|
||||
R.id.callIcon -> {
|
||||
if (SystemClock.elapsedRealtime() - lastCall > 1000) {
|
||||
lastCall = SystemClock.elapsedRealtime()
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
intent.putExtra("action", "call")
|
||||
intent.putExtra("uap", ua.uap)
|
||||
intent.putExtra("peer", peerUri)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
android.R.id.home -> {
|
||||
goBack()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
|
||||
var save = false
|
||||
for (m in chatMessages) {
|
||||
if (m.new) {
|
||||
@ -242,13 +632,10 @@ class ChatActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
if (save) Message.save()
|
||||
|
||||
imm.hideSoftInputFromWindow(newMessage.windowToken, 0)
|
||||
|
||||
keyboardController?.hide()
|
||||
BaresipService.activities.remove("chat,$aor,$peerUri,false")
|
||||
BaresipService.activities.remove("chat,$aor,$peerUri,true")
|
||||
returnResult(Activity.RESULT_OK)
|
||||
|
||||
returnResult(RESULT_OK)
|
||||
}
|
||||
|
||||
private fun returnResult(code: Int) {
|
||||
@ -256,9 +643,9 @@ class ChatActivity : AppCompatActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun uaPeerMessages(aor: String, peerUri: String): ArrayList<Message> {
|
||||
val res = ArrayList<Message>()
|
||||
for (m in Message.messages())
|
||||
private fun uaPeerMessages(aor: String, peerUri: String): List<Message> {
|
||||
val res = mutableListOf<Message>()
|
||||
for (m in BaresipService.messages.reversed())
|
||||
if ((m.aor == aor) && (m.peerUri == peerUri)) res.add(m)
|
||||
return res
|
||||
}
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
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.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class ChatListAdapter(private val ctx: Context, private val account: Account, private val rows: ArrayList<Message>) :
|
||||
ArrayAdapter<Message>(ctx, R.layout.message, rows) {
|
||||
|
||||
private val layoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private class ViewHolder(view: View?) {
|
||||
val textAvatarView: TextView = view?.findViewById(R.id.TextAvatar)!!
|
||||
val imageAvatarView: ImageView = view?.findViewById(R.id.ImageAvatar)!!
|
||||
val layoutView: LinearLayout = view?.findViewById(R.id.chat)!!
|
||||
val peerView: TextView = view?.findViewById(R.id.peer)!!
|
||||
val infoView: TextView = view?.findViewById(R.id.info)!!
|
||||
val textView: TextView = view?.findViewById(R.id.text)!!
|
||||
}
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
|
||||
val viewHolder: ViewHolder
|
||||
val rowView: View
|
||||
|
||||
if (view == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.chat_row, parent, false)
|
||||
viewHolder = ViewHolder(rowView)
|
||||
rowView.tag = viewHolder
|
||||
} else {
|
||||
rowView = view
|
||||
viewHolder = rowView.tag as ViewHolder
|
||||
}
|
||||
|
||||
val message = rows[position]
|
||||
|
||||
viewHolder.peerView.text = Utils.friendlyUri(ctx, message.peerUri, account)
|
||||
Utils.setAvatar(ctx, viewHolder.imageAvatarView, viewHolder.textAvatarView,
|
||||
if (Contact.contactName(message.peerUri) == message.peerUri)
|
||||
Utils.e164Uri(message.peerUri, account.countryCode)
|
||||
else
|
||||
message.peerUri)
|
||||
|
||||
if (message.direction == MESSAGE_DOWN)
|
||||
viewHolder.layoutView.setBackgroundResource(R.drawable.message_in_bg)
|
||||
else
|
||||
viewHolder.layoutView.setBackgroundResource(R.drawable.message_out_bg)
|
||||
|
||||
val cal = GregorianCalendar()
|
||||
cal.timeInMillis = message.timeStamp
|
||||
val fmt: DateFormat = if (isToday(message.timeStamp))
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
else
|
||||
DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
viewHolder.infoView.text = fmt.format(cal.time)
|
||||
if (message.direction == MESSAGE_UP_FAIL) {
|
||||
val info: String = if (message.responseCode != 0)
|
||||
"${viewHolder.infoView.text} - ${ctx.getString(R.string.message_failed)}: " +
|
||||
"${message.responseCode} ${message.responseReason}"
|
||||
else
|
||||
"${viewHolder.infoView.text} - ${ctx.getString(R.string.sending_failed)}"
|
||||
viewHolder.infoView.text = info
|
||||
viewHolder.infoView.setTextColor(ContextCompat.getColor(ctx, R.color.colorAccent))
|
||||
}
|
||||
|
||||
viewHolder.textView.text = message.message
|
||||
if (message.new)
|
||||
viewHolder.textView.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
return rowView
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,38 +1,98 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils.isToday
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tutpro.baresip.databinding.ActivityChatsBinding
|
||||
import com.tutpro.baresip.BaresipService.Companion.contactNames
|
||||
import com.tutpro.baresip.CustomElements.ImageAvatar
|
||||
import com.tutpro.baresip.CustomElements.Text
|
||||
import com.tutpro.baresip.CustomElements.TextAvatar
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import java.text.DateFormat
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
class ChatsActivity: AppCompatActivity() {
|
||||
class ChatsActivity: ComponentActivity() {
|
||||
|
||||
private lateinit var binding: ActivityChatsBinding
|
||||
private lateinit var uaMessages: ArrayList<Message>
|
||||
private lateinit var listView: ListView
|
||||
private lateinit var clAdapter: ChatListAdapter
|
||||
internal lateinit var peerUri: AutoCompleteTextView
|
||||
private lateinit var plusButton: ImageButton
|
||||
internal lateinit var aor: String
|
||||
internal lateinit var account: Account
|
||||
private lateinit var chatRequest: ActivityResultLauncher<Intent>
|
||||
private var scrollPosition = -1
|
||||
private var _uaMessages = mutableStateOf<List<Message>>(emptyList())
|
||||
private var uaMessages : List<Message> by _uaMessages
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
@ -44,172 +104,536 @@ class ChatsActivity: AppCompatActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityChatsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
if (Build.VERSION.SDK_INT >= 35)
|
||||
binding.ChatsView.updatePadding(top = systemBars.top + 56)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
if (!Utils.isDarkTheme(this))
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
|
||||
|
||||
listView = binding.chats
|
||||
plusButton = binding.plusButton
|
||||
enableEdgeToEdge()
|
||||
|
||||
aor = intent.extras!!.getString("aor")!!
|
||||
Utils.addActivity("chats,$aor")
|
||||
account = UserAgent.ofAor(aor)!!.account
|
||||
|
||||
val headerView = binding.account
|
||||
val headerText = getString(R.string.account) + " " +
|
||||
if (account.nickName != "")
|
||||
account.nickName
|
||||
else
|
||||
aor.split(":")[1]
|
||||
headerView.text = headerText
|
||||
val title = getString(R.string.chats)
|
||||
|
||||
uaMessages = uaMessages(aor)
|
||||
clAdapter = ChatListAdapter(this, account, uaMessages)
|
||||
listView.adapter = clAdapter
|
||||
listView.isLongClickable = true
|
||||
_uaMessages.value = uaMessages(aor)
|
||||
|
||||
chatRequest =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
clAdapter.clear()
|
||||
uaMessages = uaMessages(aor)
|
||||
clAdapter = ChatListAdapter(this, account, uaMessages)
|
||||
listView.adapter = clAdapter
|
||||
}
|
||||
if (it.resultCode == RESULT_OK)
|
||||
_uaMessages.value = uaMessages(aor)
|
||||
}
|
||||
|
||||
listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ ->
|
||||
scrollPosition = pos
|
||||
val i = Intent(this, ChatActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
b.putString("peer", uaMessages[pos].peerUri)
|
||||
i.putExtras(b)
|
||||
chatRequest.launch(i)
|
||||
}
|
||||
|
||||
listView.onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, pos, _ ->
|
||||
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
val i = Intent(this, ContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", uaMessages[pos].peerUri)
|
||||
i.putExtras(b)
|
||||
startActivity(i)
|
||||
}
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
val peerUri = uaMessages[pos].peerUri
|
||||
val messages = ArrayList<Message>()
|
||||
for (m in Message.messages())
|
||||
if ((m.aor != aor) || (m.peerUri != peerUri))
|
||||
messages.add(m)
|
||||
else
|
||||
clAdapter.remove(m)
|
||||
clAdapter.notifyDataSetChanged()
|
||||
BaresipService.messages = messages
|
||||
Message.save()
|
||||
uaMessages = uaMessages(aor)
|
||||
}
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
ChatsScreen(this, title) { goBack() }
|
||||
}
|
||||
}
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(this@ChatsActivity, R.style.AlertDialogTheme)
|
||||
val peer = Utils.friendlyUri(this@ChatsActivity, uaMessages[pos].peerUri, account)
|
||||
if (!Contact.nameExists(peer, false))
|
||||
with (builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(String.format(getString(R.string.long_chat_question),
|
||||
peer))
|
||||
setNeutralButton(getText(R.string.cancel), dialogClickListener)
|
||||
setNegativeButton(getText(R.string.delete), dialogClickListener)
|
||||
setPositiveButton(getText(R.string.add_contact), dialogClickListener)
|
||||
show()
|
||||
}
|
||||
else
|
||||
with (builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(String.format(getString(R.string.short_chat_question), peer))
|
||||
setNeutralButton(getText(R.string.cancel), dialogClickListener)
|
||||
setNegativeButton(getText(R.string.delete), dialogClickListener)
|
||||
show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
peerUri = binding.peer
|
||||
peerUri.threshold = 2
|
||||
peerUri.setAdapter(ArrayAdapter(this, android.R.layout.select_dialog_item,
|
||||
Contact.contactNames()))
|
||||
|
||||
plusButton.setOnClickListener {
|
||||
makeChat(true)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
}
|
||||
|
||||
private fun makeChat(lookForContact: Boolean = true) {
|
||||
var uriText = peerUri.text.toString().trim()
|
||||
if (uriText.isNotEmpty()) {
|
||||
@Composable
|
||||
fun ChatsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = { TopAppBar(title, navigateBack) },
|
||||
bottomBar = { NewChatPeer() },
|
||||
content = { contentPadding ->
|
||||
ChatsContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TopAppBar(title: String, navigateBack: () -> Unit) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val delete = String.format(getString(R.string.delete))
|
||||
val disable = String.format(getString(R.string.disable_history))
|
||||
val enable = String.format(getString(R.string.enable_history))
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { expanded = !expanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
CustomElements.DropdownMenu(expanded,
|
||||
{ expanded = false },
|
||||
listOf(delete, if (account.callHistory) disable else enable),
|
||||
onItemClick = { selectedItem ->
|
||||
expanded = false
|
||||
when (selectedItem) {
|
||||
delete ->
|
||||
with(
|
||||
MaterialAlertDialogBuilder(
|
||||
this@ChatsActivity,
|
||||
R.style.AlertDialogTheme
|
||||
)
|
||||
) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(String.format(getString(R.string.delete_chats_alert),
|
||||
aor.substringAfter(":")))
|
||||
setPositiveButton(delete) { dialog, _ ->
|
||||
Message.clearMessagesOfAor(aor)
|
||||
Message.save()
|
||||
_uaMessages.value = listOf()
|
||||
account.unreadMessages = false
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNeutralButton(getText(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
Account(account)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Chats(ctx, account)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Account(account: Account) {
|
||||
val headerText = getString(R.string.account) + " " +
|
||||
if (account.nickName.value != "")
|
||||
account.nickName.value
|
||||
else
|
||||
aor.split(":")[1]
|
||||
Text(
|
||||
text = headerText, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 8.dp),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Chats(ctx: Context, account: Account) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
)
|
||||
.background(LocalCustomColors.current.background),
|
||||
reverseLayout = true,
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(items = uaMessages, key = { message -> message.timeStamp }) { message ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.combinedClickable(
|
||||
onClick = {
|
||||
val i = Intent(ctx, ChatActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
b.putString("peer", message.peerUri)
|
||||
i.putExtras(b)
|
||||
chatRequest.launch(i)
|
||||
},
|
||||
onLongClick = {
|
||||
val dialogClickListener =
|
||||
DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
val i =
|
||||
Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", message.peerUri)
|
||||
i.putExtras(b)
|
||||
startActivity(i)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
Message.deleteAorPeerMessages(aor, message.peerUri)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(
|
||||
this@ChatsActivity,
|
||||
R.style.AlertDialogTheme
|
||||
)
|
||||
val peer = Utils.friendlyUri(ctx, message.peerUri, account)
|
||||
if (!Contact.nameExists(peer, BaresipService.contacts, false))
|
||||
with(builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.long_chat_question),
|
||||
peer
|
||||
)
|
||||
)
|
||||
setNeutralButton(
|
||||
getText(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setNegativeButton(
|
||||
getText(R.string.delete),
|
||||
dialogClickListener
|
||||
)
|
||||
setPositiveButton(
|
||||
getText(R.string.add_contact),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
else
|
||||
with(builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.short_chat_question),
|
||||
peer
|
||||
)
|
||||
)
|
||||
setNeutralButton(
|
||||
getText(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setNegativeButton(
|
||||
getText(R.string.delete),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
}
|
||||
)) {
|
||||
when (val contact = Contact.findContact(message.peerUri)) {
|
||||
is Contact.BaresipContact -> {
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
ImageAvatar(avatarImage)
|
||||
else
|
||||
TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
|
||||
is Contact.AndroidContact -> {
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null)
|
||||
AsyncImage(
|
||||
model = thumbNailUri,
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
else
|
||||
TextAvatar(contact.name, contact.color)
|
||||
}
|
||||
|
||||
null -> {
|
||||
val avatarImage = BitmapFactory
|
||||
.decodeResource(ctx.resources, R.drawable.person_image)
|
||||
ImageAvatar(avatarImage)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
val i = Intent(this@ChatsActivity, ChatActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
b.putString("peer", message.peerUri)
|
||||
i.putExtras(b)
|
||||
chatRequest.launch(i) },
|
||||
shape = if (message.direction == MESSAGE_DOWN)
|
||||
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
|
||||
else
|
||||
RoundedCornerShape(20.dp, 50.dp, 20.dp, 10.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor =
|
||||
if (message.direction == MESSAGE_DOWN)
|
||||
LocalCustomColors.current.secondaryLight
|
||||
else
|
||||
LocalCustomColors.current.primaryLight),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(start = 8.dp, end = 16.dp)
|
||||
) {
|
||||
val peer = Utils.friendlyUri(ctx, message.peerUri, account)
|
||||
val cal = GregorianCalendar()
|
||||
cal.timeInMillis = message.timeStamp
|
||||
val fmt: DateFormat = if (isToday(message.timeStamp))
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
else
|
||||
DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
val info = fmt.format(cal.time)
|
||||
Column {
|
||||
Row {
|
||||
Text(
|
||||
peer, color = LocalCustomColors.current.dark,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
info, color = LocalCustomColors.current.dark,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Text(
|
||||
text = message.message, color = LocalCustomColors.current.dark,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewChatPeer() {
|
||||
val suggestions by remember { contactNames }
|
||||
var filteredSuggestions by remember { mutableStateOf(suggestions) }
|
||||
var showSuggestions by remember { mutableStateOf(false) }
|
||||
val lazyListState = rememberLazyListState()
|
||||
val focusManager = LocalFocusManager.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
var newPeer by remember { mutableStateOf("") }
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (showSuggestions && filteredSuggestions.isNotEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.shadow(8.dp, RoundedCornerShape(8.dp))
|
||||
.background(LocalCustomColors.current.grayLight,
|
||||
shape = RoundedCornerShape(8.dp))
|
||||
.animateContentSize()
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 180.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
state = lazyListState
|
||||
) {
|
||||
items(
|
||||
items = filteredSuggestions,
|
||||
key = { suggestion -> suggestion }
|
||||
) { suggestion ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
newPeer = suggestion
|
||||
showSuggestions = false
|
||||
}
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = suggestion,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = LocalCustomColors.current.grayDark,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = newPeer,
|
||||
placeholder = {
|
||||
Text(text = getString(R.string.new_chat_peer))
|
||||
},
|
||||
onValueChange = {
|
||||
newPeer = it
|
||||
showSuggestions = newPeer.length > 2
|
||||
filteredSuggestions = if (it.isEmpty()) {
|
||||
suggestions
|
||||
} else {
|
||||
suggestions.filter { suggestion ->
|
||||
newPeer.length > 2 && suggestion.startsWith(
|
||||
newPeer,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(end = 6.dp)
|
||||
.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (newPeer.isNotEmpty())
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
newPeer = ""
|
||||
showSuggestions = false
|
||||
}
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = getString(R.string.new_chat_peer),
|
||||
fontSize = 18.sp
|
||||
)
|
||||
},
|
||||
textStyle = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = LocalCustomColors.current.itemText
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
SmallFloatingActionButton(
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
onClick = {
|
||||
showSuggestions = false
|
||||
if (makeChat(newPeer, true)) {
|
||||
newPeer = ""
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
},
|
||||
containerColor = LocalCustomColors.current.accent,
|
||||
contentColor = LocalCustomColors.current.background
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
modifier = Modifier.size(36.dp),
|
||||
contentDescription = stringResource(R.string.add)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeChat(peer: String, lookForContact: Boolean = true): Boolean {
|
||||
var peerUri = peer.trim()
|
||||
if (peerUri.isNotEmpty()) {
|
||||
if (lookForContact) {
|
||||
val uris = Contact.contactUris(uriText)
|
||||
val uris = Contact.contactUris(peerUri)
|
||||
if (uris.size == 1)
|
||||
uriText = uris[0]
|
||||
peerUri = uris[0]
|
||||
else if (uris.size > 1) {
|
||||
val builder = MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
|
||||
with(builder) {
|
||||
setTitle("Choose Destination")
|
||||
setItems(uris.toTypedArray()) { _, which ->
|
||||
peerUri.setText(uris[which])
|
||||
makeChat(false)
|
||||
peerUri = uris[which]
|
||||
makeChat(peerUri, false)
|
||||
}
|
||||
setNeutralButton(getString(R.string.cancel)) { _: DialogInterface, _: Int -> }
|
||||
show()
|
||||
}
|
||||
return
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (Utils.isTelNumber(uriText))
|
||||
uriText = "tel:$uriText"
|
||||
val uri = if (Utils.isTelUri(uriText)) {
|
||||
if (Utils.isTelNumber(peerUri))
|
||||
peerUri = "tel:$peerUri"
|
||||
val uri = if (Utils.isTelUri(peerUri)) {
|
||||
if (account.telProvider == "") {
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.no_telephony_provider), account.aor))
|
||||
return
|
||||
return false
|
||||
}
|
||||
Utils.telToSip(uriText, account)
|
||||
Utils.telToSip(peerUri, account)
|
||||
} else {
|
||||
Utils.uriComplete(uriText, aor)
|
||||
Utils.uriComplete(peerUri, aor)
|
||||
}
|
||||
if (!Utils.checkUri(uri)) {
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.invalid_sip_or_tel_uri), uri))
|
||||
return false
|
||||
} else {
|
||||
peerUri.text.clear()
|
||||
peerUri.isCursorVisible = false
|
||||
val i = Intent(this@ChatsActivity, ChatActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
b.putString("peer", uri)
|
||||
i.putExtras(b)
|
||||
chatRequest.launch(i)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@ -219,18 +643,7 @@ class ChatsActivity: AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
clAdapter.clear()
|
||||
uaMessages = uaMessages(aor)
|
||||
clAdapter = ChatListAdapter(this, account, uaMessages)
|
||||
listView.adapter = clAdapter
|
||||
if (uaMessages.isNotEmpty()) {
|
||||
if (scrollPosition >= 0) {
|
||||
listView.setSelection(scrollPosition)
|
||||
scrollPosition = -1
|
||||
} else {
|
||||
listView.setSelection(0)
|
||||
}
|
||||
}
|
||||
_uaMessages.value = uaMessages(aor)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@ -238,55 +651,20 @@ class ChatsActivity: AppCompatActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
when (item.itemId) {
|
||||
|
||||
R.id.delete_chats -> {
|
||||
with (MaterialAlertDialogBuilder(this@ChatsActivity, R.style.AlertDialogTheme)) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(String.format(getString(R.string.delete_chats_alert),
|
||||
aor.substringAfter(":")))
|
||||
setPositiveButton(getText(R.string.delete)) { dialog, _ ->
|
||||
Message.clear(aor)
|
||||
Message.save()
|
||||
uaMessages.clear()
|
||||
clAdapter.notifyDataSetChanged()
|
||||
account.unreadMessages = false
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNeutralButton(getText(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
android.R.id.home -> {
|
||||
goBack()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("chats,$aor")
|
||||
returnResult()
|
||||
}
|
||||
|
||||
private fun returnResult() {
|
||||
setResult(Activity.RESULT_CANCELED, Intent())
|
||||
setResult(RESULT_CANCELED, Intent())
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun uaMessages(aor: String) : ArrayList<Message> {
|
||||
val res = ArrayList<Message>()
|
||||
private fun uaMessages(aor: String) : List<Message> {
|
||||
val res = mutableListOf<Message>()
|
||||
account.unreadMessages = false
|
||||
for (m in Message.messages().reversed()) {
|
||||
for (m in BaresipService.messages) {
|
||||
if (m.aor != aor) continue
|
||||
var found = false
|
||||
for (r in res)
|
||||
@ -300,30 +678,6 @@ class ChatsActivity: AppCompatActivity() {
|
||||
account.unreadMessages = true
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun saveUaMessage(aor: String, time: Long) {
|
||||
for (i in Message.messages().indices.reversed())
|
||||
if ((Message.messages()[i].aor == aor) &&
|
||||
(Message.messages()[i].timeStamp == time)) {
|
||||
Message.messages()[i].new = false
|
||||
Message.save()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteUaMessage(aor: String, time: Long) {
|
||||
for (i in Message.messages().indices.reversed())
|
||||
if ((Message.messages()[i].aor == aor) &&
|
||||
(Message.messages()[i].timeStamp == time)) {
|
||||
Message.messages().removeAt(i)
|
||||
Message.save()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return res.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
data class Codec(val name: String, var enabled: Boolean)
|
||||
import androidx.compose.runtime.MutableState
|
||||
|
||||
data class Codec(val name: String, var enabled: MutableState<Boolean>)
|
||||
@ -1,35 +1,64 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
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.*
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
|
||||
class CodecsActivity : AppCompatActivity() {
|
||||
class CodecsActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var binding: ActivityCodecsBinding
|
||||
private lateinit var acc: Account
|
||||
private lateinit var ua: UserAgent
|
||||
private lateinit var codecsAdapter: CodecsAdapter
|
||||
|
||||
private lateinit var allCodecs: List<String>
|
||||
private lateinit var accCodecs: List<String>
|
||||
private lateinit var codecs: SnapshotStateList<Codec>
|
||||
|
||||
private var aor = ""
|
||||
private var newCodecs = ArrayList<Codec>()
|
||||
private var media = ""
|
||||
private var title = ""
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
@ -41,21 +70,7 @@ class CodecsActivity : AppCompatActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityCodecsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
if (Build.VERSION.SDK_INT >= 35)
|
||||
binding.CodecsView.updatePadding(top = systemBars.top + 64)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
if (!Utils.isDarkTheme(this))
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
enableEdgeToEdge()
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
media = intent.getStringExtra("media")!!
|
||||
@ -65,88 +80,256 @@ class CodecsActivity : AppCompatActivity() {
|
||||
ua = UserAgent.ofAor(aor)!!
|
||||
acc = ua.account
|
||||
|
||||
val allCodecs: ArrayList<String>
|
||||
val accCodecs: ArrayList<String>
|
||||
|
||||
val codecsTitle = binding.CodecsTitle
|
||||
val codecList = binding.CodecList
|
||||
|
||||
itemTouchHelper.attachToRecyclerView(codecList)
|
||||
|
||||
if (media == "audio") {
|
||||
codecsTitle.text = getString(R.string.audio_codecs)
|
||||
title = getString(R.string.audio_codecs)
|
||||
allCodecs = ArrayList(Api.audio_codecs().split(","))
|
||||
accCodecs = acc.audioCodec
|
||||
} else {
|
||||
codecsTitle.text = getString(R.string.video_codecs)
|
||||
title = getString(R.string.video_codecs)
|
||||
allCodecs = ArrayList(Api.video_codecs().split(",").distinct())
|
||||
accCodecs = acc.videoCodec
|
||||
}
|
||||
|
||||
val currentCodecs = mutableListOf<Codec>()
|
||||
for (codec in accCodecs)
|
||||
newCodecs.add(Codec(codec, true))
|
||||
currentCodecs.add(Codec(codec, mutableStateOf(true)))
|
||||
for (codec in allCodecs)
|
||||
if (codec !in accCodecs)
|
||||
newCodecs.add(Codec(codec, false))
|
||||
currentCodecs.add(Codec(codec, mutableStateOf(false)))
|
||||
|
||||
codecsAdapter = CodecsAdapter(newCodecs)
|
||||
codecs = mutableStateListOf<Codec>().apply { addAll(currentCodecs) }
|
||||
|
||||
codecList.layoutManager = LinearLayoutManager(this)
|
||||
codecList.adapter = codecsAdapter
|
||||
|
||||
itemTouchHelper.attachToRecyclerView(binding.CodecList)
|
||||
|
||||
codecsTitle.setOnClickListener {
|
||||
if (media == "audio")
|
||||
Utils.alertView(this, getString(R.string.audio_codecs),
|
||||
getString(R.string.audio_codecs_help))
|
||||
else
|
||||
Utils.alertView(this, getString(R.string.video_codecs),
|
||||
getString(R.string.video_codecs_help))
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
CodecsScreen { goBack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
}
|
||||
|
||||
private val itemTouchHelper by lazy {
|
||||
val simpleItemTouchCallback = object : SimpleCallback(
|
||||
UP or DOWN or START or END, RIGHT) {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CodecsScreen(navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = getString(R.string.codecs),
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
updateCodecs()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
tint = LocalCustomColors.current.light,
|
||||
contentDescription = "Check"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
content = { contentPadding ->
|
||||
CodecsContent(contentPadding)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@Composable
|
||||
fun CodecsContent(contentPadding: PaddingValues) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(bottom = 16.dp),
|
||||
) {
|
||||
Title()
|
||||
Codecs(codecs = codecs, onCodecsChange = { updatedCodecs ->
|
||||
onCodecsChange(updatedCodecs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCodecsChange(updatedCodecs: List<Codec>) {
|
||||
codecs.clear()
|
||||
codecs.addAll(updatedCodecs)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Title() {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.clickable {
|
||||
if (media == "audio")
|
||||
Utils.alertView(
|
||||
this, getString(R.string.audio_codecs),
|
||||
getString(R.string.audio_codecs_help)
|
||||
)
|
||||
else
|
||||
Utils.alertView(
|
||||
this, getString(R.string.video_codecs),
|
||||
getString(R.string.video_codecs_help)
|
||||
)
|
||||
},
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun Codecs(codecs: SnapshotStateList<Codec>, onCodecsChange: (List<Codec>) -> Unit) {
|
||||
|
||||
val draggableState = rememberDraggableListState(
|
||||
onMove = { fromIndex, toIndex ->
|
||||
codecs.add(toIndex, codecs.removeAt(fromIndex))
|
||||
onCodecsChange(codecs.toList())
|
||||
}
|
||||
)
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val position = viewHolder.adapterPosition
|
||||
if (newCodecs[position].enabled) {
|
||||
codecsAdapter.disableItem(position)
|
||||
codecsAdapter.notifyItemRemoved(position)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.verticalScrollbar(
|
||||
state = draggableState.listState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
state = draggableState.listState,
|
||||
contentPadding = PaddingValues(start = 12.dp, end = 12.dp),
|
||||
//verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
draggableItems(
|
||||
state = draggableState,
|
||||
items = codecs,
|
||||
key = { item -> item.name }
|
||||
) { item, isDragging ->
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = if (isDragging)
|
||||
LocalCustomColors.current.grayLight
|
||||
else
|
||||
LocalCustomColors.current.background
|
||||
),
|
||||
headlineContent = {
|
||||
Text(text = item.name,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.alpha(if (item.enabled.value) 1.0f else 0.5f)
|
||||
.padding(start = 6.dp)
|
||||
.combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
item.enabled.value = !item.enabled.value
|
||||
if (item.enabled.value) {
|
||||
val index = codecs.indexOf(item)
|
||||
codecs.removeAt(index)
|
||||
codecs.add(0, item)
|
||||
}
|
||||
else {
|
||||
val index = codecs.indexOf(item)
|
||||
codecs.removeAt(index)
|
||||
codecs.add(item)
|
||||
}
|
||||
onCodecsChange(codecs.toList())
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
modifier = Modifier.dragHandle(
|
||||
state = draggableState,
|
||||
key = item.name
|
||||
),
|
||||
imageVector =ImageVector.vectorResource(R.drawable.reorder),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
)
|
||||
if (codecs.indexOf(item) > 0)
|
||||
HorizontalDivider(
|
||||
color = LocalCustomColors.current.gray,
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
thickness = 1.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCodecs() {
|
||||
|
||||
var save = false
|
||||
val newCodecs = ArrayList<String>()
|
||||
|
||||
for (codec in codecs)
|
||||
if (codec.enabled.value)
|
||||
newCodecs.add(codec.name)
|
||||
|
||||
val codecList = Utils.implode(newCodecs, ",")
|
||||
|
||||
if (media == "audio")
|
||||
if (newCodecs != acc.audioCodec) {
|
||||
if (Api.account_set_audio_codecs(acc.accp, codecList) == 0) {
|
||||
Log.d(TAG, "New audio codecs '$codecList'")
|
||||
acc.audioCodec = newCodecs
|
||||
save = true
|
||||
} else {
|
||||
codecsAdapter.enableItem(position)
|
||||
codecsAdapter.notifyDataSetChanged()
|
||||
Log.e(TAG, "Setting of audio codecs '$codecList' failed")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
recyclerView.post { codecsAdapter.notifyDataSetChanged() }
|
||||
if (media == "video")
|
||||
if (newCodecs != acc.videoCodec) {
|
||||
if (Api.account_set_video_codecs(acc.accp, codecList) == 0) {
|
||||
Log.d(TAG, "New video codecs '$codecs'")
|
||||
acc.videoCodec = newCodecs
|
||||
save = true
|
||||
} else {
|
||||
Log.e(TAG, "Setting of video codecs '$codecs' failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemTouchHelper(simpleItemTouchCallback)
|
||||
if (save)
|
||||
AccountsActivity.saveAccounts()
|
||||
|
||||
BaresipService.activities.remove("codecs,$aor,$media")
|
||||
finish()
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
@ -156,65 +339,6 @@ class CodecsActivity : AppCompatActivity() {
|
||||
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
if (BaresipService.activities.indexOf("codecs,$aor,$media") == -1)
|
||||
return true
|
||||
|
||||
when (item.itemId) {
|
||||
|
||||
R.id.checkIcon -> {
|
||||
|
||||
var save = false
|
||||
val codecs = ArrayList<String>()
|
||||
|
||||
for (codec in newCodecs)
|
||||
if (codec.enabled)
|
||||
codecs.add(codec.name)
|
||||
|
||||
val codecList = Utils.implode(codecs, ",")
|
||||
|
||||
if (media == "audio")
|
||||
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 '$codecList' failed")
|
||||
}
|
||||
}
|
||||
|
||||
if (media == "video")
|
||||
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 '$codecs' failed")
|
||||
}
|
||||
}
|
||||
|
||||
if (save)
|
||||
AccountsActivity.saveAccounts()
|
||||
|
||||
BaresipService.activities.remove("codecs,$aor,$media")
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
android.R.id.home -> {
|
||||
goBack()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("codecs,$aor,$media")
|
||||
finish()
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
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<Codec>):
|
||||
RecyclerView.Adapter<CodecsAdapter.CodecViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CodecViewHolder {
|
||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.codec, parent, false)
|
||||
return CodecViewHolder(v)
|
||||
}
|
||||
|
||||
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 enableItem(position: Int) {
|
||||
val codec: Codec = codecs[position]
|
||||
codec.enabled = true
|
||||
codecs.removeAt(position)
|
||||
codecs.add(0, codec)
|
||||
}
|
||||
|
||||
fun disableItem(position: Int) {
|
||||
val codec: Codec = codecs[position]
|
||||
if (codec.enabled) {
|
||||
codec.enabled = false
|
||||
codecs.removeAt(position)
|
||||
codecs.add(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.reorderImage.setImageResource(
|
||||
if (codec.enabled) R.drawable.reorder else android.R.color.transparent
|
||||
)
|
||||
}
|
||||
|
||||
class CodecViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||
var codecName: TextView = itemView.findViewById(R.id.codecName)
|
||||
var reorderImage: ImageView = itemView.findViewById(R.id.reorderImage)
|
||||
}
|
||||
}
|
||||
@ -153,11 +153,10 @@ object Config {
|
||||
if ("${module}.so" in previousModules)
|
||||
config = "${config}module ${module}.so\n"
|
||||
|
||||
if ((!Utils.isAecSupported() && !File(configPath).exists()) ||
|
||||
"webrtc_aecm.so" in previousModules) {
|
||||
Utils.aecAgcCheck()
|
||||
|
||||
if (!BaresipService.aecAvailable)
|
||||
config = "${config}module webrtc_aecm.so\n"
|
||||
BaresipService.webrtcAec = true
|
||||
}
|
||||
|
||||
val micGain = previousVariable("augain")
|
||||
config = if (BaresipService.agcAvailable || micGain == "" || micGain == "1.0")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,6 @@ const val DEFAULT_CHANNEL_ID = "com.tutpro.baresip.default"
|
||||
const val MEDIUM_CHANNEL_ID = "com.tutpro.baresip.medium"
|
||||
const val HIGH_CHANNEL_ID = "com.tutpro.baresip.high"
|
||||
|
||||
const val CONTACTS_PERMISSION_REQUEST_CODE = 1
|
||||
|
||||
const val STATUS_NOTIFICATION_ID = 101
|
||||
const val CALL_NOTIFICATION_ID = 102
|
||||
const val CALL_MISSED_NOTIFICATION_ID = 103
|
||||
|
||||
@ -7,33 +7,61 @@ import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.net.toUri
|
||||
import com.tutpro.baresip.BaresipService.Companion.contactNames
|
||||
import java.io.File
|
||||
import java.util.ArrayList
|
||||
|
||||
sealed class Contact {
|
||||
|
||||
class BaresipContact(var name: String, var uri: String, var color: Int, val id: Long,
|
||||
class BaresipContact(var name: String, var uri: String, var color: Int, var id: Long,
|
||||
var favorite: Boolean): Contact() {
|
||||
var avatarImage: Bitmap? = null
|
||||
}
|
||||
|
||||
class AndroidContact(var name: String, var color: Int, var thumbnailUri: Uri?,
|
||||
var favorite: Boolean): Contact() {
|
||||
var id: Long, var favorite: Boolean): Contact() {
|
||||
val uris = ArrayList<String>()
|
||||
}
|
||||
|
||||
fun name(): String {
|
||||
return when (this) {
|
||||
is AndroidContact -> name
|
||||
is BaresipContact -> name
|
||||
}
|
||||
}
|
||||
|
||||
fun id(): Long {
|
||||
return when (this) {
|
||||
is AndroidContact -> id
|
||||
is BaresipContact -> id
|
||||
}
|
||||
}
|
||||
|
||||
fun favorite(): Boolean {
|
||||
return when (this) {
|
||||
is AndroidContact -> favorite
|
||||
is BaresipContact -> favorite
|
||||
}
|
||||
}
|
||||
|
||||
fun copy(): Contact {
|
||||
val copy = when (this) {
|
||||
is BaresipContact ->
|
||||
BaresipContact(name, uri, color, id, favorite)
|
||||
is AndroidContact ->
|
||||
AndroidContact(name, color, thumbnailUri, id, favorite)
|
||||
}
|
||||
when (this) {
|
||||
is BaresipContact ->
|
||||
(copy as BaresipContact).avatarImage = avatarImage
|
||||
is AndroidContact ->
|
||||
(copy as AndroidContact).uris.addAll(uris)
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val CONTACTS_SIZE = 256
|
||||
|
||||
fun contacts(): ArrayList<Contact> {
|
||||
return BaresipService.contacts
|
||||
}
|
||||
|
||||
fun contactNames(): ArrayList<String> {
|
||||
return BaresipService.contactNames
|
||||
}
|
||||
|
||||
// Return contact name of uri or uri itself if contact with uri is not found
|
||||
fun contactName(uri: String): String {
|
||||
var contact = findContact(uri)
|
||||
@ -42,21 +70,29 @@ sealed class Contact {
|
||||
if (Utils.isTelNumber(userPart))
|
||||
contact = findContact("tel:$userPart")
|
||||
}
|
||||
if (contact != null) {
|
||||
return when (contact) {
|
||||
is BaresipContact ->
|
||||
contact.name
|
||||
is AndroidContact ->
|
||||
contact.name
|
||||
}
|
||||
}
|
||||
if (contact != null)
|
||||
return contact.name()
|
||||
return uri
|
||||
}
|
||||
|
||||
fun baresipContact(name: String): BaresipContact? {
|
||||
for (c in BaresipService.baresipContacts.value)
|
||||
if (c.name == name)
|
||||
return c
|
||||
return null
|
||||
}
|
||||
|
||||
fun androidContact(name: String): AndroidContact? {
|
||||
for (c in BaresipService.androidContacts.value)
|
||||
if (c.name == name)
|
||||
return c
|
||||
return null
|
||||
}
|
||||
|
||||
// Return URIs of contact name
|
||||
fun contactUris(name: String): ArrayList<String> {
|
||||
val uris = ArrayList<String>()
|
||||
for (c in contacts())
|
||||
for (c in BaresipService.contacts)
|
||||
when (c) {
|
||||
is BaresipContact -> {
|
||||
if (c.name.equals(name, ignoreCase = true)) {
|
||||
@ -78,7 +114,7 @@ sealed class Contact {
|
||||
}
|
||||
|
||||
fun findContact(uri: String): Contact? {
|
||||
for (c in contacts())
|
||||
for (c in BaresipService.contacts)
|
||||
when (c) {
|
||||
is BaresipContact -> {
|
||||
if (Utils.uriMatch(c.uri, uri))
|
||||
@ -95,22 +131,16 @@ sealed class Contact {
|
||||
return null
|
||||
}
|
||||
|
||||
fun nameExists(name: String, ignoreCase: Boolean): Boolean {
|
||||
for (c in BaresipService.contacts)
|
||||
when (c) {
|
||||
is BaresipContact ->
|
||||
if (c.name.equals(name, ignoreCase = ignoreCase))
|
||||
return true
|
||||
is AndroidContact ->
|
||||
if (c.name.equals(name, ignoreCase = ignoreCase))
|
||||
return true
|
||||
}
|
||||
fun nameExists(name: String, list: List<Contact>, ignoreCase: Boolean): Boolean {
|
||||
for (c in list)
|
||||
if (c.name().equals(name, ignoreCase = ignoreCase))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun saveBaresipContacts() {
|
||||
var contents = ""
|
||||
for (c in BaresipService.baresipContacts)
|
||||
for (c in BaresipService.baresipContacts.value)
|
||||
contents += "\"${c.name}\" <${c.uri}>;id=${c.id};color=${c.color}" +
|
||||
";favorite=${if (c.favorite) "yes" else "no"}\n"
|
||||
Utils.putFileContents(BaresipService.filesPath + "/contacts",
|
||||
@ -129,7 +159,7 @@ sealed class Contact {
|
||||
ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'"
|
||||
val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection,
|
||||
selection, null, null)
|
||||
BaresipService.androidContacts.clear()
|
||||
BaresipService.androidContacts.value = listOf()
|
||||
val contacts = HashMap<Long, AndroidContact>()
|
||||
while (cur != null && cur.moveToNext()) {
|
||||
val id = cur.getLong(0)
|
||||
@ -141,7 +171,7 @@ sealed class Contact {
|
||||
val contact = if (contacts.containsKey(id))
|
||||
contacts[id]!!
|
||||
else
|
||||
AndroidContact(name, Utils.randomColor(), thumb, starred == 1)
|
||||
AndroidContact(name, Utils.randomColor(), thumb, id, starred == 1)
|
||||
if (contact.name == "" && name != "")
|
||||
contact.name = name
|
||||
if (contact.thumbnailUri == null && thumb != null)
|
||||
@ -158,9 +188,11 @@ sealed class Contact {
|
||||
contacts[id] = contact
|
||||
}
|
||||
cur?.close()
|
||||
val newList = mutableListOf<AndroidContact>()
|
||||
for ((_, value) in contacts)
|
||||
if (value.name != "" && value.uris.isNotEmpty())
|
||||
BaresipService.androidContacts.add(value)
|
||||
newList.add(value)
|
||||
BaresipService.androidContacts.value = newList.toList()
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@ -179,7 +211,8 @@ sealed class Contact {
|
||||
val contacts = String(content)
|
||||
var contactNo = 0
|
||||
val baseId = System.currentTimeMillis()
|
||||
BaresipService.baresipContacts.clear()
|
||||
val restored = mutableListOf<BaresipContact>()
|
||||
BaresipService.baresipContacts.value = listOf()
|
||||
contacts.lines().forEach {
|
||||
val parts = it.split("\"")
|
||||
if (parts.size == 3) {
|
||||
@ -212,55 +245,62 @@ sealed class Contact {
|
||||
Log.e(TAG, "Could not read avatar image from '$id.png")
|
||||
}
|
||||
}
|
||||
BaresipService.baresipContacts.add(contact)
|
||||
restored.add(contact)
|
||||
}
|
||||
}
|
||||
BaresipService.baresipContacts.value = restored.toList()
|
||||
return true
|
||||
}
|
||||
|
||||
fun contactsUpdate() {
|
||||
BaresipService.contacts.clear()
|
||||
val newList = mutableListOf<Contact>()
|
||||
if (BaresipService.contactsMode != "android")
|
||||
BaresipService.contacts.addAll(BaresipService.baresipContacts)
|
||||
for (c in BaresipService.baresipContacts.value)
|
||||
newList.add(c.copy())
|
||||
if (BaresipService.contactsMode != "baresip")
|
||||
addAndroidContacts()
|
||||
sortContacts()
|
||||
for (c in BaresipService.androidContacts.value)
|
||||
if (!nameExists(c.name, newList, true))
|
||||
newList.add(c.copy())
|
||||
newList.sortBy{ when (it) {
|
||||
is BaresipContact -> if (it.favorite) "0" + it.name else "1" + it.name
|
||||
is AndroidContact -> if (it.favorite) "0" + it.name else "1" + it.name
|
||||
}}
|
||||
BaresipService.contacts = newList.toList()
|
||||
generateContactNames()
|
||||
BaresipService.contactUpdate.postValue(System.nanoTime())
|
||||
//BaresipService.contactUpdate.postValue(System.nanoTime())
|
||||
}
|
||||
|
||||
fun addBaresipContact(contact: BaresipContact) {
|
||||
BaresipService.baresipContacts.add(contact)
|
||||
val updatedList = BaresipService.baresipContacts.value.toMutableList()
|
||||
updatedList.add(contact)
|
||||
BaresipService.baresipContacts.value = updatedList.toList()
|
||||
}
|
||||
|
||||
fun updateBaresipContact(contact: BaresipContact) {
|
||||
val updatedList = BaresipService.baresipContacts.value.toMutableList()
|
||||
val contactCopy = contact.copy()
|
||||
updatedList.removeIf { it.id == contact.id }
|
||||
updatedList.add(contactCopy as BaresipContact)
|
||||
BaresipService.baresipContacts.value = updatedList.toList()
|
||||
}
|
||||
|
||||
fun removeBaresipContact(contact: BaresipContact) {
|
||||
BaresipService.baresipContacts.remove(contact)
|
||||
val updatedList = BaresipService.baresipContacts.value.toMutableList()
|
||||
updatedList.removeIf { it.id == contact.id }
|
||||
BaresipService.baresipContacts.value = updatedList.toList()
|
||||
saveBaresipContacts()
|
||||
}
|
||||
|
||||
private fun generateContactNames () {
|
||||
BaresipService.contactNames.clear()
|
||||
val newList = mutableListOf<String>()
|
||||
for (c in BaresipService.contacts)
|
||||
when (c) {
|
||||
is BaresipContact ->
|
||||
BaresipService.contactNames.add(c.name)
|
||||
newList.add(c.name)
|
||||
is AndroidContact ->
|
||||
BaresipService.contactNames.add(c.name)
|
||||
newList.add(c.name)
|
||||
}
|
||||
contactNames.value = newList.toList()
|
||||
}
|
||||
|
||||
private fun addAndroidContacts() {
|
||||
for (c in BaresipService.androidContacts)
|
||||
if (!nameExists(c.name, true))
|
||||
BaresipService.contacts.add(c)
|
||||
}
|
||||
|
||||
private fun sortContacts() {
|
||||
BaresipService.contacts.sortBy{ when (it) {
|
||||
is BaresipContact -> if (it.favorite) "0" + it.name else "1" + it.name
|
||||
is AndroidContact -> if (it.favorite) "0" + it.name else "1" + it.name
|
||||
}}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,524 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.CommonDataKinds
|
||||
import android.provider.ContactsContract.Contacts.Data
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.tutpro.baresip.databinding.ActivityContactBinding
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
|
||||
class ContactActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityContactBinding
|
||||
private lateinit var layout: LinearLayout
|
||||
private lateinit var textAvatarView: TextView
|
||||
private lateinit var cardAvatarView: CardView
|
||||
private lateinit var cardImageAvatarView: ImageView
|
||||
private lateinit var nameView: EditText
|
||||
private lateinit var uriView: EditText
|
||||
private lateinit var favoriteCheck: CheckBox
|
||||
private lateinit var androidCheck: CheckBox
|
||||
private lateinit var menu: Menu
|
||||
|
||||
private var newContact = false
|
||||
private var newAvatar = ""
|
||||
private var uOrI = ""
|
||||
|
||||
private var index = 0
|
||||
private var color = 0
|
||||
private var id: Long = 0
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityContactBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
layout = binding.ContactView
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
if (Build.VERSION.SDK_INT >= 35)
|
||||
binding.ContactView.updatePadding(top = systemBars.top + 56)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
if (!Utils.isDarkTheme(this))
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
|
||||
textAvatarView = binding.TextAvatar
|
||||
cardAvatarView = binding.CardAvatar
|
||||
cardImageAvatarView = binding.ImageAvatar
|
||||
nameView = binding.Name
|
||||
uriView = binding.Uri
|
||||
favoriteCheck = binding.Favorite
|
||||
androidCheck = binding.Android
|
||||
|
||||
newContact = intent.getBooleanExtra("new", false)
|
||||
|
||||
if (newContact) {
|
||||
if (BaresipService.contactsMode == "baresip") {
|
||||
binding.AndroidTitle.visibility = View.GONE
|
||||
androidCheck.visibility = View.GONE
|
||||
}
|
||||
title = getString(R.string.new_contact)
|
||||
color = Utils.randomColor()
|
||||
id = System.currentTimeMillis()
|
||||
showTextAvatar("?", color)
|
||||
nameView.setText("")
|
||||
nameView.hint = getString(R.string.contact_name)
|
||||
nameView.setSelection(nameView.text.length)
|
||||
val uri = intent.getStringExtra("uri")!!
|
||||
if (uri == "")
|
||||
uriView.setText("")
|
||||
else
|
||||
uriView.setText(uri)
|
||||
uOrI = uri
|
||||
favoriteCheck.isChecked = false
|
||||
if ((BaresipService.contactsMode == "android")) {
|
||||
androidCheck.isChecked = true
|
||||
androidCheck.isClickable = false
|
||||
} else {
|
||||
androidCheck.isChecked = false
|
||||
}
|
||||
} else {
|
||||
binding.AndroidTitle.visibility = View.GONE
|
||||
androidCheck.visibility = View.GONE
|
||||
index = intent.getIntExtra("index", 0)
|
||||
val contact = Contact.contacts()[index] as Contact.BaresipContact
|
||||
val name = contact.name
|
||||
color = contact.color
|
||||
id = contact.id
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
showImageAvatar(avatarImage)
|
||||
else
|
||||
showTextAvatar(name, color)
|
||||
title = name
|
||||
nameView.setText(name)
|
||||
uriView.setText(contact.uri)
|
||||
favoriteCheck.isChecked = contact.favorite
|
||||
uOrI = index.toString()
|
||||
}
|
||||
|
||||
val avatarRequest =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.also { uri ->
|
||||
try {
|
||||
val inputStream = baseContext.contentResolver.openInputStream(uri)
|
||||
val avatarBitmap = BitmapFactory.decodeStream(inputStream)
|
||||
inputStream?.close()
|
||||
val scaledBitmap = Bitmap.createScaledBitmap(avatarBitmap, 192, 192, true)
|
||||
val exif = ExifInterface(baseContext.contentResolver.openInputStream(uri)!!)
|
||||
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL)
|
||||
val rotatedBitmap = rotateBitmap(scaledBitmap, orientation)
|
||||
showImageAvatar(rotatedBitmap)
|
||||
if (Utils.saveBitmap(rotatedBitmap, File(BaresipService.filesPath, "tmp.png")))
|
||||
newAvatar = "image"
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Could not read avatar image: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textAvatarView.setOnClickListener {
|
||||
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "image/*"
|
||||
}
|
||||
avatarRequest.launch(intent)
|
||||
|
||||
}
|
||||
|
||||
textAvatarView.setOnLongClickListener {
|
||||
|
||||
color = Utils.randomColor()
|
||||
showTextAvatar(textAvatarView.text.toString(), color)
|
||||
newAvatar = "text"
|
||||
true
|
||||
|
||||
}
|
||||
|
||||
cardAvatarView.setOnClickListener {
|
||||
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "image/*"
|
||||
}
|
||||
avatarRequest.launch(intent)
|
||||
|
||||
}
|
||||
|
||||
cardAvatarView.setOnLongClickListener {
|
||||
|
||||
color = Utils.randomColor()
|
||||
showTextAvatar(nameView.text.toString(), color)
|
||||
newAvatar = "text"
|
||||
true
|
||||
|
||||
}
|
||||
|
||||
binding.AndroidTitle.setOnClickListener {
|
||||
Utils.alertView(this, getString(R.string.android),
|
||||
getString(R.string.android_contact_help))
|
||||
}
|
||||
|
||||
// Log.d(TAG, "Android sip contacts ${logAndroidContacts(this, "sip")}")
|
||||
// Log.d(TAG, "Android tel contacts ${logAndroidContacts(this, "tel")}")
|
||||
|
||||
Utils.addActivity("contact,$newContact,$uOrI")
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(optionsMenu: Menu): Boolean {
|
||||
|
||||
super.onCreateOptionsMenu(optionsMenu)
|
||||
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.check_icon, optionsMenu)
|
||||
menu = optionsMenu
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
if (BaresipService.activities.indexOf("contact,$newContact,$uOrI") == -1) return true
|
||||
|
||||
when (item.itemId) {
|
||||
|
||||
R.id.checkIcon -> {
|
||||
|
||||
var newName = nameView.text.toString().trim()
|
||||
var newUri = uriView.text.toString().filterNot{setOf('-', ' ', '(', ')').contains(it)}
|
||||
|
||||
if (newName == "") newName = newUri
|
||||
|
||||
if (!Utils.checkName(newName)) {
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.invalid_contact), newName))
|
||||
return false
|
||||
}
|
||||
|
||||
val alert: Boolean = if (newContact)
|
||||
Contact.nameExists(newName, true)
|
||||
else {
|
||||
val c = Contact.contacts()[index] as Contact.BaresipContact
|
||||
(c.name != newName) && Contact.nameExists(newName, false)
|
||||
}
|
||||
if (alert) {
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.contact_already_exists), newName))
|
||||
return false
|
||||
}
|
||||
|
||||
if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:"))
|
||||
newUri = if (Utils.isTelNumber(newUri))
|
||||
"tel:$newUri"
|
||||
else
|
||||
"sip:$newUri"
|
||||
if (!Utils.checkUri(newUri)) {
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.invalid_sip_or_tel_uri), newUri))
|
||||
return false
|
||||
}
|
||||
|
||||
val contact: Contact.BaresipContact
|
||||
|
||||
if (newContact) {
|
||||
if (Contact.contacts().size >= Contact.CONTACTS_SIZE) {
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.contacts_exceeded),
|
||||
Contact.CONTACTS_SIZE))
|
||||
BaresipService.activities.removeAt(0)
|
||||
return true
|
||||
} else {
|
||||
contact = Contact.BaresipContact(newName, newUri, color, id, favoriteCheck.isChecked)
|
||||
}
|
||||
} else {
|
||||
contact = Contact.contacts()[index] as Contact.BaresipContact
|
||||
contact.uri = newUri
|
||||
contact.name = newName
|
||||
contact.color = color
|
||||
contact.favorite = favoriteCheck.isChecked
|
||||
}
|
||||
|
||||
when (newAvatar) {
|
||||
"text" -> {
|
||||
if (contact.avatarImage != null) {
|
||||
contact.avatarImage = null
|
||||
Utils.deleteFile(File(BaresipService.filesPath, "${contact.id}.png"))
|
||||
}
|
||||
}
|
||||
"image" -> {
|
||||
contact.avatarImage = (cardImageAvatarView.drawable as BitmapDrawable).bitmap
|
||||
Utils.deleteFile(File(BaresipService.filesPath, "${contact.id}.png"))
|
||||
File(BaresipService.filesPath, "tmp.png")
|
||||
.renameTo(File(BaresipService.filesPath, "${contact.id}.png"))
|
||||
}
|
||||
}
|
||||
|
||||
if (androidCheck.isChecked) {
|
||||
addOrUpdateAndroidContact(this, contact)
|
||||
} else {
|
||||
if (newContact)
|
||||
Contact.addBaresipContact(contact)
|
||||
Contact.saveBaresipContacts()
|
||||
Contact.contactsUpdate()
|
||||
}
|
||||
|
||||
BaresipService.activities.remove("contact,$newContact,$uOrI")
|
||||
|
||||
val i = Intent(this, MainActivity::class.java)
|
||||
if (androidCheck.isChecked && contact.favorite)
|
||||
i.putExtra("name", newName)
|
||||
setResult(Activity.RESULT_OK, i)
|
||||
finish()
|
||||
}
|
||||
|
||||
android.R.id.home -> {
|
||||
goBack()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
BaresipService.activities.remove("contact,$newContact,$uOrI")
|
||||
setResult(Activity.RESULT_CANCELED, Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun showTextAvatar(name: String, color: Int) {
|
||||
textAvatarView.visibility = View.VISIBLE
|
||||
cardAvatarView.visibility = View.GONE
|
||||
textAvatarView.background.setTint(color)
|
||||
textAvatarView.text = "${name[0]}"
|
||||
}
|
||||
|
||||
private fun showImageAvatar(image: Bitmap) {
|
||||
textAvatarView.visibility = View.GONE
|
||||
cardAvatarView.visibility = View.VISIBLE
|
||||
cardImageAvatarView.setImageBitmap(image)
|
||||
}
|
||||
|
||||
private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap {
|
||||
val matrix = Matrix()
|
||||
when (orientation) {
|
||||
ExifInterface.ORIENTATION_NORMAL -> return bitmap
|
||||
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
|
||||
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
|
||||
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
|
||||
matrix.setRotate(180f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
||||
matrix.setRotate(90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
|
||||
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
||||
matrix.setRotate(-90f)
|
||||
matrix.postScale(-1f, 1f)
|
||||
}
|
||||
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
|
||||
else -> return bitmap
|
||||
}
|
||||
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
|
||||
bitmap.width, bitmap.height, matrix, true)
|
||||
bitmap.recycle()
|
||||
return rotatedBitmap
|
||||
}
|
||||
|
||||
private fun addOrUpdateAndroidContact(ctx: Context, contact: Contact.BaresipContact) {
|
||||
val projection = arrayOf(ContactsContract.Data.RAW_CONTACT_ID)
|
||||
val selection = ContactsContract.Data.MIMETYPE + "='" +
|
||||
CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " +
|
||||
CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'"
|
||||
val c: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection,
|
||||
selection, null, null)
|
||||
if (c != null && c.moveToFirst()) {
|
||||
updateAndroidContact(c.getLong(0), contact)
|
||||
} else {
|
||||
addAndroidContact(ctx, contact)
|
||||
}
|
||||
c?.close()
|
||||
}
|
||||
|
||||
private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Boolean {
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
|
||||
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null).build())
|
||||
ops.add(ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
|
||||
.build())
|
||||
val mimeType = if (contact.uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
ops.add(ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, mimeType)
|
||||
.withValue(Data.DATA1, contact.uri.substringAfter(":"))
|
||||
.build())
|
||||
|
||||
if (contact.avatarImage != null) {
|
||||
val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!)
|
||||
if (photoData != null) {
|
||||
ops.add(ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.Photo.PHOTO, photoData)
|
||||
.build())
|
||||
}
|
||||
}
|
||||
try {
|
||||
ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of contact ${contact.name} failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateAndroidContact(rawContactId: Long, contact: Contact.BaresipContact) {
|
||||
if (updateAndroidUri(rawContactId, contact.uri) == 0)
|
||||
addAndroidUri(rawContactId, contact.uri)
|
||||
if (updateAndroidPhoto(rawContactId, contact.avatarImage) == 0)
|
||||
if (contact.avatarImage != null)
|
||||
addAndroidPhoto(rawContactId, contact.avatarImage!!)
|
||||
}
|
||||
|
||||
private fun addAndroidUri(rawContactId: Long, uri: String) {
|
||||
val mimeType = if (uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
||||
.withValue(Data.MIMETYPE, mimeType)
|
||||
.withValue(Data.DATA1, uri.substringAfter(":"))
|
||||
.build())
|
||||
try {
|
||||
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of SIP URI $uri failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAndroidUri(rawContactId: Long, uri: String): Int {
|
||||
val mimeType = if (uri.startsWith("sip:"))
|
||||
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
||||
else
|
||||
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(ContactsContract.Data.DATA1, uri)
|
||||
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
||||
"${ContactsContract.Data.MIMETYPE}='$mimeType'"
|
||||
return try {
|
||||
contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Update of Android URI $uri failed")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAndroidPhoto(rawContactId: Long, photoBits: Bitmap) {
|
||||
val photoBytes = bitmapToPNGByteArray(photoBits)
|
||||
if (photoBytes != null) {
|
||||
val ops = ArrayList<ContentProviderOperation>()
|
||||
ops.add(ContentProviderOperation
|
||||
.newInsert(ContactsContract.Data.CONTENT_URI)
|
||||
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
||||
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
||||
.withValue(CommonDataKinds.Photo.PHOTO, photoBytes)
|
||||
.build())
|
||||
try {
|
||||
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Adding of Android photo failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAndroidPhoto(rawContactId: Long, photoBits: Bitmap?): Int {
|
||||
val photoBytes = if (photoBits == null)
|
||||
null
|
||||
else
|
||||
bitmapToPNGByteArray(photoBits)
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes)
|
||||
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
||||
"${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'"
|
||||
return try {
|
||||
contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "updateAndroidPhoto failed")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun bitmapToPNGByteArray(bitmap: Bitmap): ByteArray? {
|
||||
val size = bitmap.width * bitmap.height * 4
|
||||
val out = ByteArrayOutputStream(size)
|
||||
return try {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
out.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to serialize photo: $e")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,218 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.provider.ContactsContract
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class ContactListAdapter(private val ctx: Context, private val rows: ArrayList<Contact>,
|
||||
private val aor: String) :
|
||||
ArrayAdapter<Contact>(ctx, R.layout.contact_row, rows) {
|
||||
|
||||
private val layoutInflater = LayoutInflater.from(context)
|
||||
private var lastClick: Long = 0
|
||||
|
||||
private class ViewHolder(view: View?) {
|
||||
val textAvatarView: TextView = view?.findViewById(R.id.TextAvatar)!!
|
||||
val imageAvatarView: ImageView = view?.findViewById(R.id.ImageAvatar)!!
|
||||
val nameView: TextView = view?.findViewById(R.id.contactName)!!
|
||||
val actionView: ImageButton = view?.findViewById(R.id.edit)!!
|
||||
}
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
|
||||
val viewHolder: ViewHolder
|
||||
val rowView: View
|
||||
|
||||
if (view == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.contact_row, parent, false)
|
||||
viewHolder = ViewHolder(rowView)
|
||||
rowView.tag = viewHolder
|
||||
} else {
|
||||
rowView = view
|
||||
viewHolder = rowView.tag as ViewHolder
|
||||
}
|
||||
|
||||
val contact = rows[position]
|
||||
|
||||
if (contact is Contact.BaresipContact) {
|
||||
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null) {
|
||||
viewHolder.imageAvatarView.setImageBitmap(avatarImage)
|
||||
} else {
|
||||
viewHolder.textAvatarView.background.setTint(contact.color)
|
||||
if (contact.name.isNotEmpty())
|
||||
viewHolder.textAvatarView.text = "${contact.name[0]}"
|
||||
else
|
||||
viewHolder.textAvatarView.text = ""
|
||||
viewHolder.imageAvatarView.setImageBitmap(Utils.bitmapFromView(viewHolder.textAvatarView))
|
||||
}
|
||||
|
||||
viewHolder.nameView.text = contact.name
|
||||
viewHolder.nameView.textSize = 20f
|
||||
if (contact.favorite)
|
||||
viewHolder.nameView.setTypeface(null, Typeface.ITALIC)
|
||||
else
|
||||
viewHolder.nameView.setTypeface(null, Typeface.NORMAL)
|
||||
viewHolder.nameView.setPadding(6, 6, 0, 6)
|
||||
|
||||
if (aor != "") {
|
||||
viewHolder.nameView.setOnClickListener {
|
||||
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> {
|
||||
val i = Intent(ctx, MainActivity::class.java)
|
||||
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
if (which == DialogInterface.BUTTON_NEGATIVE)
|
||||
i.putExtra("action", "call")
|
||||
else
|
||||
i.putExtra("action", "message")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null) {
|
||||
Log.w(TAG, "onClickListener did not find AoR $aor")
|
||||
} else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", contact.uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
with(MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(String.format(ctx.getString(R.string.contact_action_question),
|
||||
contact.name))
|
||||
setNeutralButton(ctx.getText(R.string.cancel), dialogClickListener)
|
||||
setNegativeButton(ctx.getText(R.string.call), dialogClickListener)
|
||||
setPositiveButton(ctx.getText(R.string.send_message), dialogClickListener)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewHolder.actionView.visibility = View.VISIBLE
|
||||
viewHolder.actionView.setOnClickListener {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
val i = Intent(ctx, ContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", false)
|
||||
b.putInt("index", position)
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i, null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (contact is Contact.AndroidContact) {
|
||||
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null) {
|
||||
viewHolder.imageAvatarView.setImageURI(thumbNailUri)
|
||||
} else {
|
||||
viewHolder.textAvatarView.background.setTint(contact.color)
|
||||
if (contact.name.isNotEmpty())
|
||||
viewHolder.textAvatarView.text = "${contact.name[0]}"
|
||||
else
|
||||
viewHolder.textAvatarView.text = ""
|
||||
viewHolder.imageAvatarView.setImageBitmap(Utils.bitmapFromView(viewHolder.textAvatarView))
|
||||
}
|
||||
|
||||
viewHolder.nameView.text = contact.name
|
||||
viewHolder.nameView.textSize = 20f
|
||||
if (contact.favorite)
|
||||
viewHolder.nameView.setTypeface(null, Typeface.ITALIC)
|
||||
else
|
||||
viewHolder.nameView.setTypeface(null, Typeface.NORMAL)
|
||||
viewHolder.nameView.setPadding(6, 6, 0, 6)
|
||||
|
||||
viewHolder.nameView.setOnClickListener {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
val i = Intent(ctx, AndroidContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
b.putInt("index", position)
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i, null)
|
||||
}
|
||||
}
|
||||
|
||||
viewHolder.actionView.visibility = View.GONE
|
||||
}
|
||||
|
||||
viewHolder.nameView.setOnLongClickListener {
|
||||
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
when (contact) {
|
||||
is Contact.BaresipContact -> {
|
||||
val id = contact.id
|
||||
val avatarFile = File(BaresipService.filesPath, "$id.png")
|
||||
if (avatarFile.exists()) {
|
||||
try {
|
||||
avatarFile.delete()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Could not delete file '$id.png")
|
||||
}
|
||||
}
|
||||
Contact.removeBaresipContact(contact)
|
||||
Contact.contactsUpdate()
|
||||
this.notifyDataSetChanged()
|
||||
}
|
||||
is Contact.AndroidContact -> {
|
||||
deleteAndroidContact(ctx, contact.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
with(MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(String.format(ctx.getString(R.string.contact_delete_question),
|
||||
when (contact) {
|
||||
is Contact.BaresipContact -> contact.name
|
||||
is Contact.AndroidContact -> contact.name
|
||||
}))
|
||||
setNeutralButton(ctx.getText(R.string.cancel), dialogClickListener)
|
||||
setPositiveButton(ctx.getText(R.string.delete), dialogClickListener)
|
||||
show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
return rowView
|
||||
}
|
||||
|
||||
private fun deleteAndroidContact(ctx: Context, name: String): Int {
|
||||
return ctx.contentResolver.delete(ContactsContract.RawContacts.CONTENT_URI,
|
||||
ContactsContract.Contacts.DISPLAY_NAME + "='" + name + "'",
|
||||
null)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,28 +1,71 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.os.Build
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.provider.ContactsContract
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Observer
|
||||
import com.tutpro.baresip.databinding.ActivityContactsBinding
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tutpro.baresip.CustomElements.TextAvatar
|
||||
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class ContactsActivity : AppCompatActivity() {
|
||||
class ContactsActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var binding: ActivityContactsBinding
|
||||
private lateinit var clAdapter: ContactListAdapter
|
||||
private lateinit var aor: String
|
||||
private var newAndroidName: String? = null
|
||||
private var lastClick: Long = 0
|
||||
@ -33,32 +76,26 @@ class ContactsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val contactRequest =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
if (it.data != null && it.data!!.hasExtra("name"))
|
||||
newAndroidName = it.data!!.getStringExtra("name")
|
||||
}
|
||||
Contact.contactsUpdate()
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityContactsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
enableEdgeToEdge()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v: View, insets: WindowInsetsCompat ->
|
||||
val systemBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
if (Build.VERSION.SDK_INT >= 35)
|
||||
binding.ContactsView.updatePadding(top = systemBars.top + 56)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
if (!Utils.isDarkTheme(this))
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightStatusBars = true
|
||||
val title = String.format(getString(R.string.contacts))
|
||||
|
||||
aor = intent.getStringExtra("aor")!!
|
||||
Utils.addActivity("contacts,$aor")
|
||||
|
||||
val listView = binding.contacts
|
||||
clAdapter = ContactListAdapter(this, BaresipService.contacts, aor)
|
||||
listView.adapter = clAdapter
|
||||
listView.isLongClickable = true
|
||||
|
||||
val androidContactsObserver = Observer<Long> {
|
||||
if (newAndroidName != null) {
|
||||
val contentValues = ContentValues()
|
||||
@ -73,64 +110,374 @@ class ContactsActivity : AppCompatActivity() {
|
||||
}
|
||||
newAndroidName = null
|
||||
}
|
||||
clAdapter.notifyDataSetChanged()
|
||||
Contact.contactsUpdate()
|
||||
}
|
||||
BaresipService.contactUpdate.observe(this, androidContactsObserver)
|
||||
|
||||
val contactRequest =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
if (it.data != null && it.data!!.hasExtra("name"))
|
||||
newAndroidName = it.data!!.getStringExtra("name")
|
||||
}
|
||||
clAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
val plusButton = binding.plusButton
|
||||
plusButton.setOnClickListener {
|
||||
if (Contact.contacts().size >= Contact.CONTACTS_SIZE) {
|
||||
Utils.alertView(this, getString(R.string.notice),
|
||||
String.format(getString(R.string.contacts_exceeded),
|
||||
Contact.CONTACTS_SIZE))
|
||||
} else {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
val intent = Intent(this, ContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", "")
|
||||
intent.putExtras(b)
|
||||
contactRequest.launch(intent)
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = LocalCustomColors.current.background
|
||||
) {
|
||||
ContactsScreen(LocalContext.current, title) {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
clAdapter.notifyDataSetChanged()
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ContactsScreen(ctx: Context, title: String, navigateBack: () -> Unit) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.imePadding()
|
||||
.safeDrawingPadding(),
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = LocalCustomColors.current.light,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = LocalCustomColors.current.primary
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = LocalCustomColors.current.light
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
SmallFloatingActionButton(onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
val intent = Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", "")
|
||||
intent.putExtras(b)
|
||||
contactRequest.launch(intent)
|
||||
}},
|
||||
containerColor = LocalCustomColors.current.accent,
|
||||
contentColor = LocalCustomColors.current.background
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Add,
|
||||
modifier = Modifier.size(36.dp),
|
||||
contentDescription = stringResource(R.string.add)
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
ContactsContent(ctx, contentPadding)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsContent(ctx: Context, contentPadding: PaddingValues) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LocalCustomColors.current.background)
|
||||
.padding(contentPadding)
|
||||
.padding(start = 16.dp, end = 4.dp, top = 16.dp, bottom = 64.dp)
|
||||
.verticalScrollbar(
|
||||
state = lazyListState,
|
||||
width = 4.dp,
|
||||
color = LocalCustomColors.current.gray
|
||||
),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
items(BaresipService.contacts, key = { contact -> contact.id() }) { contact ->
|
||||
|
||||
val name = contact.name()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
when(contact) {
|
||||
|
||||
is Contact.BaresipContact -> {
|
||||
val avatarImage = contact.avatarImage
|
||||
if (avatarImage != null)
|
||||
Image(
|
||||
bitmap = avatarImage.asImageBitmap(),
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
else
|
||||
TextAvatar(name, contact.color)
|
||||
}
|
||||
|
||||
is Contact.AndroidContact -> {
|
||||
val thumbNailUri = contact.thumbnailUri
|
||||
if (thumbNailUri != null)
|
||||
AsyncImage(
|
||||
model = thumbNailUri,
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
else
|
||||
TextAvatar(name, contact.color)
|
||||
}
|
||||
}
|
||||
|
||||
when(contact) {
|
||||
|
||||
is Contact.BaresipContact -> {
|
||||
Text(text = name,
|
||||
fontSize = 20.sp,
|
||||
fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 10.dp)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
val dialogClickListener =
|
||||
DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE, DialogInterface.BUTTON_NEGATIVE -> {
|
||||
val i = Intent(
|
||||
ctx,
|
||||
MainActivity::class.java
|
||||
)
|
||||
i.flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
if (which == DialogInterface.BUTTON_NEGATIVE)
|
||||
i.putExtra("action", "call")
|
||||
else
|
||||
i.putExtra("action", "message")
|
||||
val ua = UserAgent.ofAor(aor)
|
||||
if (ua == null) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"onClickListener did not find AoR $aor"
|
||||
)
|
||||
} else {
|
||||
BaresipService.activities.clear()
|
||||
i.putExtra("uap", ua.uap)
|
||||
i.putExtra("peer", contact.uri)
|
||||
(ctx as Activity).startActivity(i)
|
||||
}
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
with(
|
||||
MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
|
||||
) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
ctx.getString(R.string.contact_action_question),
|
||||
name
|
||||
)
|
||||
)
|
||||
setNeutralButton(
|
||||
ctx.getText(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setNegativeButton(
|
||||
ctx.getText(R.string.call),
|
||||
dialogClickListener
|
||||
)
|
||||
setPositiveButton(
|
||||
ctx.getText(R.string.send_message),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
val dialogClickListener =
|
||||
DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
val id = contact.id
|
||||
val avatarFile = File(
|
||||
BaresipService.filesPath,
|
||||
"$id.png"
|
||||
)
|
||||
if (avatarFile.exists()) {
|
||||
try {
|
||||
avatarFile.delete()
|
||||
} catch (e: IOException) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Could not delete file '$id.png"
|
||||
)
|
||||
}
|
||||
}
|
||||
Contact.removeBaresipContact(contact)
|
||||
Contact.contactsUpdate()
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
with(
|
||||
MaterialAlertDialogBuilder(
|
||||
ctx,
|
||||
R.style.AlertDialogTheme
|
||||
)
|
||||
) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
ctx.getString(R.string.contact_delete_question),
|
||||
name
|
||||
)
|
||||
)
|
||||
setNeutralButton(
|
||||
ctx.getText(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setPositiveButton(
|
||||
ctx.getText(R.string.delete),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
SmallFloatingActionButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
val intent = Intent(ctx, BaresipContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", false)
|
||||
b.putString("name", name)
|
||||
intent.putExtras(b)
|
||||
contactRequest.launch(intent)
|
||||
}
|
||||
},
|
||||
containerColor = LocalCustomColors.current.background,
|
||||
contentColor = LocalCustomColors.current.secondary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Edit,
|
||||
modifier = Modifier.size(28.dp),
|
||||
contentDescription = stringResource(R.string.add)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is Contact.AndroidContact -> {
|
||||
Text(text = name,
|
||||
fontSize = 20.sp,
|
||||
fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal,
|
||||
color = LocalCustomColors.current.itemText,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 10.dp, top = 4.dp, bottom = 4.dp)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (SystemClock.elapsedRealtime() - lastClick > 1000) {
|
||||
lastClick = SystemClock.elapsedRealtime()
|
||||
val i =
|
||||
Intent(ctx, AndroidContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putString("aor", aor)
|
||||
b.putString("name", name)
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i, null)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
val dialogClickListener =
|
||||
DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
ctx.contentResolver.delete(
|
||||
ContactsContract.RawContacts.CONTENT_URI,
|
||||
ContactsContract.Contacts.DISPLAY_NAME + "='" + name + "'",
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
with(
|
||||
MaterialAlertDialogBuilder(
|
||||
ctx,
|
||||
R.style.AlertDialogTheme
|
||||
)
|
||||
) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(
|
||||
String.format(
|
||||
ctx.getString(R.string.contact_delete_question),
|
||||
name
|
||||
)
|
||||
)
|
||||
setNeutralButton(
|
||||
ctx.getText(R.string.cancel),
|
||||
dialogClickListener
|
||||
)
|
||||
setPositiveButton(
|
||||
ctx.getText(R.string.delete),
|
||||
dialogClickListener
|
||||
)
|
||||
show()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
when (item.itemId) {
|
||||
android.R.id.home ->
|
||||
goBack()
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
|
||||
BaresipService.activities.remove("contacts,$aor")
|
||||
setResult(Activity.RESULT_OK, Intent())
|
||||
setResult(RESULT_OK, Intent())
|
||||
finish()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
177
app/src/main/kotlin/com/tutpro/baresip/CustomColors.kt
Normal file
177
app/src/main/kotlin/com/tutpro/baresip/CustomColors.kt
Normal file
@ -0,0 +1,177 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
|
||||
@Immutable
|
||||
data class CustomColors(
|
||||
val primary: Color = Color.Unspecified,
|
||||
val primaryDark: Color = Color.Unspecified,
|
||||
val primaryLight: Color = Color.Unspecified,
|
||||
val secondary: Color = Color.Unspecified,
|
||||
val secondaryDark: Color = Color.Unspecified,
|
||||
val secondaryLight: Color = Color.Unspecified,
|
||||
val gray: Color = Color.Unspecified,
|
||||
val grayDark: Color = Color.Unspecified,
|
||||
val grayLight: Color = Color.Unspecified,
|
||||
val accent: Color = Color.Unspecified,
|
||||
val baresip: Color = Color.Unspecified,
|
||||
val white: Color = Color.Unspecified,
|
||||
val black: Color = Color.Unspecified,
|
||||
val strong: Color = Color.Unspecified,
|
||||
val green: Color = Color.Unspecified,
|
||||
val red: Color = Color.Unspecified,
|
||||
val trafficGreen: Color = Color.Unspecified,
|
||||
val trafficYellow: Color = Color.Unspecified,
|
||||
val trafficRed: Color = Color.Unspecified,
|
||||
val light: Color = Color.Unspecified,
|
||||
val dark: Color = Color.Unspecified,
|
||||
val spinnerText: Color = Color.Unspecified,
|
||||
val spinnerDropdown: Color = Color.Unspecified,
|
||||
val spinnerDivider: Color = Color.Unspecified,
|
||||
val itemText: Color = Color.Unspecified,
|
||||
val alertText: Color = Color.Unspecified,
|
||||
val codec: Color = Color.Unspecified,
|
||||
val background: Color = Color.Unspecified,
|
||||
val popupBackground: Color = Color.Unspecified,
|
||||
val alert: Color = Color.Unspecified,
|
||||
val actionbar: Color = Color.Unspecified,
|
||||
)
|
||||
|
||||
val light_primary = Color(0xff0ca1fd)
|
||||
val light_primary_dark = Color(0xff0073c9)
|
||||
val light_primary_light = Color(0xff6ad2ff)
|
||||
val light_secondary = Color(0xFF00B9A1)
|
||||
val light_secondary_dark = Color(0xFF008873)
|
||||
val light_secondary_light = Color(0xff5cecd2)
|
||||
val light_gray = Color(0xff9e9e9e)
|
||||
val light_gray_dark = Color(0xFF424242)
|
||||
val light_gray_light = Color(0xffe0e0e0)
|
||||
val light_accent = Color(0xFFF77445)
|
||||
val light_baresip = Color(0xff0ca1fd)
|
||||
val light_white = Color(0xffffffff)
|
||||
val light_black = Color(0xFF000000)
|
||||
val light_strong = light_black
|
||||
val light_green = Color(0xff01df01)
|
||||
val light_red = Color(0xffff0000)
|
||||
val light_traffic_green = Color(0xFF4CAF50)
|
||||
val light_traffic_yellow = Color(0xFFFFEB3B)
|
||||
val light_traffic_red = Color(0xFFFF5722)
|
||||
val light_light = light_white
|
||||
val light_dark = Color(0xFF383838)
|
||||
val light_spinner_text = light_dark
|
||||
val light_spinner_dropdown = light_gray_light
|
||||
val light_spinner_divider = light_gray
|
||||
val light_item_text = light_black
|
||||
val light_alert_text = light_black
|
||||
val light_codec = light_black
|
||||
val light_background = Color(0xfffcfcfc)
|
||||
val light_popup_background = light_secondary_dark
|
||||
val light_alert = Color(0xff0ca1fd) // == light_primary
|
||||
val light_actionbar = light_background
|
||||
|
||||
val dark_primary = Color(0xff0073c9)
|
||||
val dark_primary_dark = Color(0xff00008b)
|
||||
val dark_primary_light = Color(0xff0ca1fd)
|
||||
val dark_secondary = Color(0xFF00B9A1)
|
||||
val dark_secondary_dark = Color(0xFF008873)
|
||||
val dark_secondary_light = Color(0xff5cecd2)
|
||||
val dark_gray = Color(0xff9e9e9e)
|
||||
val dark_gray_dark = Color(0xFF424242)
|
||||
val dark_gray_light = Color(0xffe0e0e0)
|
||||
val dark_accent = Color(0xFFF77445)
|
||||
val dark_baresip = Color(0xff0ca1fd)
|
||||
val dark_white = Color(0xffffffff)
|
||||
val dark_black = Color(0xFF000000)
|
||||
val dark_strong = dark_white
|
||||
val dark_green = Color(0xff01df01)
|
||||
val dark_red = Color(0xffdf0101)
|
||||
val dark_traffic_green = Color(0xFF2E7D32)
|
||||
val dark_traffic_yellow = Color(0xFFF9A825)
|
||||
val dark_traffic_red = Color(0xFFC62828)
|
||||
val dark_light = dark_gray_light
|
||||
val dark_dark = Color(0xFF383838)
|
||||
val dark_spinner_text = dark_dark
|
||||
val dark_spinner_dropdown = dark_gray
|
||||
val dark_spinner_divider = dark_gray_dark
|
||||
val dark_item_text = dark_gray_light
|
||||
val dark_alert_text = dark_gray_light
|
||||
val dark_codec = dark_secondary_dark
|
||||
val dark_background = dark_dark
|
||||
val dark_popup_background = dark_secondary_dark
|
||||
val dark_alert = dark_primary_light
|
||||
val dark_actionbar = dark_gray_dark
|
||||
|
||||
val LightCustomColors = CustomColors(
|
||||
primary = light_primary,
|
||||
primaryDark = light_primary_dark,
|
||||
primaryLight = light_primary_light,
|
||||
secondary = light_secondary,
|
||||
secondaryDark = light_secondary_dark,
|
||||
secondaryLight = light_secondary_light,
|
||||
gray = light_gray,
|
||||
grayDark = light_gray_dark,
|
||||
grayLight = light_gray_light,
|
||||
accent = light_accent,
|
||||
baresip = light_baresip,
|
||||
white = light_white,
|
||||
black = light_black,
|
||||
strong = light_strong,
|
||||
green = light_green,
|
||||
red = light_red,
|
||||
trafficGreen = light_traffic_green,
|
||||
trafficYellow = light_traffic_yellow,
|
||||
trafficRed = light_traffic_red,
|
||||
light = light_light,
|
||||
dark = light_dark,
|
||||
spinnerText = light_spinner_text,
|
||||
spinnerDropdown = light_spinner_dropdown,
|
||||
spinnerDivider = light_spinner_divider,
|
||||
itemText = light_item_text,
|
||||
alertText = light_alert_text,
|
||||
codec = light_codec,
|
||||
background = light_background,
|
||||
popupBackground = light_popup_background,
|
||||
alert = light_alert,
|
||||
actionbar = light_actionbar,
|
||||
)
|
||||
|
||||
val DarkCustomColors = CustomColors(
|
||||
primary = dark_primary,
|
||||
primaryDark = dark_primary_dark,
|
||||
primaryLight = dark_primary_light,
|
||||
secondary = dark_secondary,
|
||||
secondaryDark = dark_secondary_dark,
|
||||
secondaryLight = dark_secondary_light,
|
||||
gray = dark_gray,
|
||||
grayDark = dark_gray_dark,
|
||||
grayLight = dark_gray_light,
|
||||
accent = dark_accent,
|
||||
baresip = dark_baresip,
|
||||
white = dark_white,
|
||||
black = dark_black,
|
||||
strong = dark_strong,
|
||||
green = dark_green,
|
||||
red = dark_red,
|
||||
trafficGreen = dark_traffic_green,
|
||||
trafficYellow = dark_traffic_yellow,
|
||||
trafficRed = dark_traffic_red,
|
||||
light = dark_light,
|
||||
dark = dark_dark,
|
||||
spinnerText = dark_spinner_text,
|
||||
spinnerDropdown = dark_spinner_dropdown,
|
||||
spinnerDivider = dark_spinner_divider,
|
||||
itemText = dark_item_text,
|
||||
alertText = dark_alert_text,
|
||||
codec = dark_codec,
|
||||
background = dark_background,
|
||||
popupBackground = dark_popup_background,
|
||||
alert = dark_alert,
|
||||
actionbar = dark_actionbar,
|
||||
)
|
||||
|
||||
val LocalCustomColors = compositionLocalOf { CustomColors() }
|
||||
317
app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt
Normal file
317
app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt
Normal file
@ -0,0 +1,317 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
|
||||
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.content.ContextCompat.getString
|
||||
import com.tutpro.baresip.CustomElements.Text
|
||||
import com.tutpro.baresip.CustomElements.ThreeButtonAlertDialog
|
||||
|
||||
object CustomElements {
|
||||
|
||||
@Composable
|
||||
fun Text(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = LocalCustomColors.current.itemText,
|
||||
fontSize: TextUnit = 16.sp,
|
||||
fontWeight: FontWeight = FontWeight.Normal,
|
||||
textAlign: TextAlign? = null,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
) {
|
||||
androidx.compose.material3.Text(
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
fontWeight = fontWeight,
|
||||
textAlign = textAlign,
|
||||
maxLines = maxLines
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PullToRefreshBox(
|
||||
isRefreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
enabled: Boolean = true,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
val refreshState = rememberPullToRefreshState()
|
||||
|
||||
Box(
|
||||
modifier.pullToRefresh(
|
||||
state = refreshState,
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
enabled = enabled,
|
||||
),
|
||||
contentAlignment = contentAlignment,
|
||||
) {
|
||||
content()
|
||||
Indicator(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
isRefreshing = isRefreshing,
|
||||
state = refreshState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DropdownMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
items: List<String>,
|
||||
onItemClick: (String) -> Unit
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
containerColor = LocalCustomColors.current.grayLight,
|
||||
modifier = Modifier.background(
|
||||
LocalCustomColors.current.popupBackground.copy(alpha = 0.95f)
|
||||
)
|
||||
) {
|
||||
val itemsIterator = items.iterator()
|
||||
while (itemsIterator.hasNext()) {
|
||||
val item = itemsIterator.next()
|
||||
DropdownMenuItem(
|
||||
text = { Text(item, color = LocalCustomColors.current.light) },
|
||||
onClick = { onItemClick(item) }
|
||||
)
|
||||
if (itemsIterator.hasNext())
|
||||
HorizontalDivider(color = LocalCustomColors.current.spinnerDivider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Checkbox(checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = LocalCustomColors.current.accent,
|
||||
uncheckedColor = LocalCustomColors.current.strong,
|
||||
checkmarkColor = LocalCustomColors.current.strong,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextAvatar(name: String, color: Int) {
|
||||
Box(
|
||||
modifier = Modifier.size(36.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(SolidColor(Color(color)))
|
||||
}
|
||||
val text = if (name == "") "" else name[0].toString()
|
||||
Text(text, color = Color.White, fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageAvatar(bitmap: Bitmap) {
|
||||
Image(
|
||||
bitmap = bitmap.asImageBitmap(),
|
||||
contentDescription = "Avatar",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawDrawable(
|
||||
drawableId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
tint: Color? = null
|
||||
) {
|
||||
val image = painterResource(id = drawableId)
|
||||
Image(
|
||||
painter = image,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
colorFilter = if (tint != null) ColorFilter.tint(tint) else null
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.verticalScrollbar(
|
||||
state: LazyListState,
|
||||
width: Dp = 8.dp,
|
||||
alwaysShow: Boolean = true,
|
||||
color: Color = LocalCustomColors.current.gray
|
||||
): Modifier {
|
||||
val targetAlpha = if (state.isScrollInProgress || alwaysShow) 1f else 0f
|
||||
val duration = if (state.isScrollInProgress) 150 else 500
|
||||
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = targetAlpha,
|
||||
animationSpec = tween(durationMillis = duration)
|
||||
)
|
||||
|
||||
return drawWithContent {
|
||||
drawContent()
|
||||
|
||||
val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index
|
||||
val needDrawScrollbar = state.isScrollInProgress || alpha > 0.0f
|
||||
|
||||
// Draw scrollbar if scrolling or if the animation is still running and lazy column has content
|
||||
if (needDrawScrollbar && firstVisibleElementIndex != null) {
|
||||
val elementHeight = this.size.height / state.layoutInfo.totalItemsCount
|
||||
val scrollbarOffsetY = firstVisibleElementIndex * elementHeight
|
||||
val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight
|
||||
|
||||
drawRect(
|
||||
color = color,
|
||||
topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY),
|
||||
size = Size(width.toPx(), scrollbarHeight),
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThreeButtonAlertDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onPositiveClick: () -> Unit,
|
||||
onNegativeClick: () -> Unit,
|
||||
onNeutralClick: () -> Unit,
|
||||
dialogTitle: String,
|
||||
dialogText: String,
|
||||
) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
text = dialogTitle
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
text = dialogText
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
TextButton(
|
||||
onClick = { onNeutralClick() },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Neutral")
|
||||
}
|
||||
TextButton(
|
||||
onClick = { onNegativeClick() },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Negative")
|
||||
}
|
||||
Button(
|
||||
onClick = { onPositiveClick() },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Positive")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ThreeButtonAlertDialogPreview() {
|
||||
var openDialog by remember { mutableStateOf(true) }
|
||||
if (openDialog) {
|
||||
ThreeButtonAlertDialog(
|
||||
onDismissRequest = { openDialog = false },
|
||||
onPositiveClick = { openDialog = false },
|
||||
onNegativeClick = { openDialog = false },
|
||||
onNeutralClick = { openDialog = false },
|
||||
dialogTitle = "My Dialog Title",
|
||||
dialogText = "This is the message text of my dialog.",
|
||||
)
|
||||
}
|
||||
}
|
||||
358
app/src/main/kotlin/com/tutpro/baresip/DraggableLazyList.kt
Normal file
358
app/src/main/kotlin/com/tutpro/baresip/DraggableLazyList.kt
Normal file
@ -0,0 +1,358 @@
|
||||
/*
|
||||
* Copyright 2024 Allan Veloso Lopes
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Temporarily implementation until this issue is made available on the official library
|
||||
// https://issuetracker.google.com/issues/356619939
|
||||
// Based on https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
|
||||
// https://issuetracker.google.com/issues/181282427
|
||||
package com.tutpro.baresip
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.zIndex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Creates a [DraggableListState] that is remembered across compositions.
|
||||
*
|
||||
* @param lazyListState the [LazyListState] whose items are being dragged.
|
||||
* @param onMove the callback that is triggered when an item is moved.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDraggableListState(
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
onMove: (Int, Int) -> Unit
|
||||
): DraggableListState {
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = remember(lazyListState) {
|
||||
DraggableListState(
|
||||
listState = lazyListState,
|
||||
onMove = onMove,
|
||||
scope = scope
|
||||
)
|
||||
}
|
||||
LaunchedEffect(state) {
|
||||
while (true) {
|
||||
val diff = state.scrollChannel.receive()
|
||||
lazyListState.scrollBy(diff)
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* A state object that can be hoisted to control and observe dragging within a list.
|
||||
*
|
||||
* In most cases, this will be created via [rememberLazyListState].
|
||||
*
|
||||
* @param listState the state of the list where the item is being dragged.
|
||||
* @param scope a coroutine scope for performing animations.
|
||||
* @param onMove the callback that is triggered when an item is moved. The callback is
|
||||
* invoked whenever the dragged item middle point passes another item top or bottom offset,
|
||||
* it does not depend on the drag to be completed.
|
||||
*/
|
||||
class DraggableListState internal constructor(
|
||||
val listState: LazyListState,
|
||||
private val scope: CoroutineScope,
|
||||
private val onMove: (Int, Int) -> Unit
|
||||
) {
|
||||
var draggingItemIndex by mutableStateOf<Int?>(null)
|
||||
private set
|
||||
|
||||
internal val scrollChannel = Channel<Float>()
|
||||
|
||||
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
|
||||
|
||||
private var draggingItemInitialOffset by mutableIntStateOf(0)
|
||||
|
||||
val draggingItemOffset: Float
|
||||
get() = draggingItemLayoutInfo?.let { item ->
|
||||
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
|
||||
} ?: 0f
|
||||
|
||||
private val draggingItemLayoutInfo: LazyListItemInfo?
|
||||
get() = listState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
|
||||
|
||||
var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
|
||||
private set
|
||||
|
||||
var previousItemOffset = Animatable(0f)
|
||||
private set
|
||||
|
||||
internal fun onDragStart(index: Int) {
|
||||
listState.layoutInfo.visibleItemsInfo
|
||||
.find { it.index == index }
|
||||
?.also {
|
||||
draggingItemIndex = it.index
|
||||
draggingItemInitialOffset = it.offset
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onDragStart(key: Any) {
|
||||
listState.layoutInfo.visibleItemsInfo
|
||||
.find { it.key == key }
|
||||
?.also {
|
||||
draggingItemIndex = it.index
|
||||
draggingItemInitialOffset = it.offset
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onDragInterrupted() {
|
||||
if (draggingItemIndex != null) {
|
||||
previousIndexOfDraggedItem = draggingItemIndex
|
||||
val startOffset = draggingItemOffset
|
||||
scope.launch {
|
||||
previousItemOffset.snapTo(startOffset)
|
||||
previousItemOffset.animateTo(
|
||||
0f,
|
||||
spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f)
|
||||
)
|
||||
previousIndexOfDraggedItem = null
|
||||
}
|
||||
}
|
||||
draggingItemDraggedDelta = 0f
|
||||
draggingItemIndex = null
|
||||
draggingItemInitialOffset = 0
|
||||
}
|
||||
|
||||
internal fun onDrag(offset: Offset) {
|
||||
draggingItemDraggedDelta += offset.y
|
||||
|
||||
val draggingItem = draggingItemLayoutInfo ?: return
|
||||
val startOffset = draggingItem.offset + draggingItemOffset
|
||||
val endOffset = startOffset + draggingItem.size
|
||||
val middleOffset = startOffset + (endOffset - startOffset) / 2f
|
||||
|
||||
val targetItem = listState.layoutInfo.visibleItemsInfo.find { item ->
|
||||
middleOffset.toInt() in item.offset..item.offsetEnd &&
|
||||
draggingItem.index != item.index
|
||||
}
|
||||
|
||||
if (targetItem != null) {
|
||||
if (
|
||||
draggingItem.index == listState.firstVisibleItemIndex ||
|
||||
targetItem.index == listState.firstVisibleItemIndex
|
||||
) {
|
||||
listState.requestScrollToItem(
|
||||
listState.firstVisibleItemIndex,
|
||||
listState.firstVisibleItemScrollOffset
|
||||
)
|
||||
}
|
||||
onMove(draggingItem.index, targetItem.index)
|
||||
draggingItemIndex = targetItem.index
|
||||
} else {
|
||||
val overscroll = when {
|
||||
draggingItemDraggedDelta > 0 ->
|
||||
(endOffset - listState.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
|
||||
|
||||
draggingItemDraggedDelta < 0 ->
|
||||
(startOffset - listState.layoutInfo.viewportStartOffset).coerceAtMost(0f)
|
||||
|
||||
else -> 0f
|
||||
}
|
||||
if (overscroll != 0f) {
|
||||
scrollChannel.trySend(overscroll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val LazyListItemInfo.offsetEnd: Int
|
||||
get() = this.offset + this.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the area that will act as the handle for dragging operations.
|
||||
*
|
||||
* @param state The draggable state of the list where the handler is being used.
|
||||
* @param index The index of the item for which the handler is being used.
|
||||
* @param onlyAfterLongPress Indicates whether dragging should begin only after a long press.
|
||||
*/
|
||||
fun Modifier.dragHandle(
|
||||
state: DraggableListState,
|
||||
index: Int,
|
||||
onlyAfterLongPress: Boolean = false
|
||||
): Modifier {
|
||||
return pointerInput(state) {
|
||||
if (onlyAfterLongPress) {
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDrag = { change, offset ->
|
||||
change.consume()
|
||||
state.onDrag(offset = offset)
|
||||
},
|
||||
onDragStart = { state.onDragStart(index = index) },
|
||||
onDragEnd = { state.onDragInterrupted() },
|
||||
onDragCancel = { state.onDragInterrupted() }
|
||||
)
|
||||
} else {
|
||||
detectDragGestures(
|
||||
onDrag = { change, offset ->
|
||||
change.consume()
|
||||
state.onDrag(offset = offset)
|
||||
},
|
||||
onDragStart = { state.onDragStart(index = index) },
|
||||
onDragEnd = { state.onDragInterrupted() },
|
||||
onDragCancel = { state.onDragInterrupted() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the area that will act as the handle for dragging operations.
|
||||
*
|
||||
* If you use this version of the [dragHandle], you must implement the key parameter on the
|
||||
* [draggableItems] or [draggableItemsIndexed].
|
||||
*
|
||||
* @param state The draggable state of the list where the handler is being used.
|
||||
* @param key The unique identifier for the item within the list for which the handler is being used.
|
||||
* @param onlyAfterLongPress Indicates whether dragging should begin only after a long press.
|
||||
*/
|
||||
fun Modifier.dragHandle(
|
||||
state: DraggableListState,
|
||||
key: Any,
|
||||
onlyAfterLongPress: Boolean = false
|
||||
): Modifier {
|
||||
return pointerInput(state) {
|
||||
if (onlyAfterLongPress) {
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDrag = { change, offset ->
|
||||
change.consume()
|
||||
state.onDrag(offset = offset)
|
||||
},
|
||||
onDragStart = { state.onDragStart(key = key) },
|
||||
onDragEnd = { state.onDragInterrupted() },
|
||||
onDragCancel = { state.onDragInterrupted() }
|
||||
)
|
||||
} else {
|
||||
detectDragGestures(
|
||||
onDrag = { change, offset ->
|
||||
change.consume()
|
||||
state.onDrag(offset = offset)
|
||||
},
|
||||
onDragStart = { state.onDragStart(key = key) },
|
||||
onDragEnd = { state.onDragInterrupted() },
|
||||
onDragCancel = { state.onDragInterrupted() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a list of draggable items.
|
||||
*
|
||||
* To the items to be draggable, it's also necessary to add the modifier [dragHandle] to
|
||||
* the preferable UI element of the item which will be the target of the drag operation.
|
||||
*
|
||||
* @param items the data list
|
||||
* @param key a factory of stable and unique keys representing the item. Using the same key
|
||||
* for multiple items in the list is not allowed. Type of the key should be saveable
|
||||
* via Bundle on Android. If null is passed the position in the list will represent the key.
|
||||
* When you specify the key the scroll position will be maintained based on the key, which
|
||||
* means if you add/remove items before the current visible item the item with the given key
|
||||
* will be kept as the first visible one. This can be overridden by calling 'requestScrollToItem'
|
||||
* on the 'LazyListState'.
|
||||
* @param contentType a factory of the content types for the item. The item compositions of
|
||||
* the same type could be reused more efficiently. Note that null is a valid type and items of such
|
||||
* type will be considered compatible.
|
||||
* @param itemContent the content displayed by a single item
|
||||
*/
|
||||
inline fun <T> LazyListScope.draggableItemsIndexed(
|
||||
state: DraggableListState,
|
||||
items: List<T>,
|
||||
noinline key: ((index: Int, item: T) -> Any)? = null,
|
||||
crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
|
||||
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T, isDragging: Boolean) -> Unit
|
||||
) = itemsIndexed(
|
||||
items = items,
|
||||
key = key,
|
||||
contentType = contentType
|
||||
) { index, item ->
|
||||
|
||||
val isDragging = index == state.draggingItemIndex
|
||||
val draggingModifier = if (isDragging) {
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.graphicsLayer { translationY = state.draggingItemOffset }
|
||||
} else if (index == state.previousIndexOfDraggedItem) {
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.graphicsLayer { translationY = state.previousItemOffset.value }
|
||||
} else {
|
||||
Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
}
|
||||
Box(modifier = draggingModifier) {
|
||||
itemContent(index, item, isDragging)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a list of draggable items.
|
||||
*
|
||||
* To the items to be draggable, it's also necessary to add the modifier [dragHandle] to
|
||||
* the preferable UI element of the item which will be the target of the drag operation.
|
||||
*
|
||||
* @param items the data list
|
||||
* @param key a factory of stable and unique keys representing the item. Using the same key
|
||||
* for multiple items in the list is not allowed. Type of the key should be saveable
|
||||
* via Bundle on Android. If null is passed the position in the list will represent the key.
|
||||
* When you specify the key the scroll position will be maintained based on the key, which
|
||||
* means if you add/remove items before the current visible item the item with the given key
|
||||
* will be kept as the first visible one. This can be overridden by calling 'requestScrollToItem'
|
||||
* on the 'LazyListState'.
|
||||
* @param contentType a factory of the content types for the item. The item compositions of
|
||||
* the same type could be reused more efficiently. Note that null is a valid type and items of such
|
||||
* type will be considered compatible.
|
||||
* @param itemContent the content displayed by a single item
|
||||
*/
|
||||
inline fun <T> LazyListScope.draggableItems(
|
||||
state: DraggableListState,
|
||||
items: List<T>,
|
||||
noinline key: ((item: T) -> Any)? = null,
|
||||
crossinline contentType: (item: T) -> Any? = { _ -> null },
|
||||
crossinline itemContent: @Composable LazyItemScope.(item: T, isDragging: Boolean) -> Unit
|
||||
) = draggableItemsIndexed(
|
||||
state = state,
|
||||
items = items,
|
||||
key = key?.let { block -> { _, item -> block(item) } },
|
||||
contentType = { _, item -> contentType(item) },
|
||||
itemContent = { _, item, isDragging -> itemContent(item, isDragging) }
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@ -8,32 +8,76 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
|
||||
var new: Boolean): Serializable {
|
||||
|
||||
fun add() {
|
||||
BaresipService.messages.add(this)
|
||||
val updatedMessages = BaresipService.messages.toMutableList()
|
||||
updatedMessages.add(this)
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
var count = 0
|
||||
var firstIndex = -1
|
||||
for (i in BaresipService.messages.indices)
|
||||
if (BaresipService.messages[i].aor == this.aor) {
|
||||
if (count == 0) firstIndex = i
|
||||
var remove: Message? = null
|
||||
for (message in BaresipService.messages)
|
||||
if (message.aor == this.aor) {
|
||||
count++
|
||||
if (count > MESSAGE_HISTORY_SIZE) {
|
||||
remove = message
|
||||
break
|
||||
}
|
||||
}
|
||||
if (count > MESSAGE_HISTORY_SIZE)
|
||||
BaresipService.messages.removeAt(firstIndex)
|
||||
if (remove != null)
|
||||
updatedMessages.remove(remove)
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
save()
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
val updatedMessages = BaresipService.messages.toMutableList()
|
||||
updatedMessages.remove(this)
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
save()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val MESSAGE_HISTORY_SIZE = 100
|
||||
|
||||
fun messages(): ArrayList<Message> {
|
||||
fun messages(): List<Message> {
|
||||
return BaresipService.messages
|
||||
}
|
||||
|
||||
fun clear(aor: String) {
|
||||
val it = BaresipService.messages.iterator()
|
||||
fun clearMessagesOfAor(aor: String) {
|
||||
val updatedMessages = BaresipService.messages.toMutableList()
|
||||
val it = updatedMessages.iterator()
|
||||
while (it.hasNext()) if (it.next().aor == aor) it.remove()
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
}
|
||||
|
||||
fun deleteAorMessage(aor: String, time: Long) {
|
||||
val updatedMessages = BaresipService.messages.toMutableList()
|
||||
for (message in updatedMessages)
|
||||
if (message.aor == aor && message.timeStamp == time) {
|
||||
updatedMessages.remove(message)
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
save()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAorPeerMessages(aor: String, peerUri: String) {
|
||||
val updatedMessages = BaresipService.messages.toMutableList()
|
||||
for (message in updatedMessages)
|
||||
if (message.aor == aor && message.peerUri == peerUri)
|
||||
updatedMessages.remove(message)
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
save()
|
||||
}
|
||||
|
||||
fun updateAorMessage(aor: String, time: Long) {
|
||||
val updatedMessages = BaresipService.messages.toMutableList()
|
||||
for (message in updatedMessages)
|
||||
if (message.aor == aor && message.timeStamp == time) {
|
||||
message.new = false
|
||||
BaresipService.messages = updatedMessages.toList()
|
||||
save()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun save() {
|
||||
|
||||
@ -1,141 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
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.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class MessageListAdapter(private val ctx: Context, private val peerUri: String,
|
||||
private val chatPeer: String, private val rows: ArrayList<Message>) :
|
||||
ArrayAdapter<Message>(ctx, R.layout.message, rows) {
|
||||
|
||||
private val layoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private class ViewHolder(view: View?) {
|
||||
val layoutView: LinearLayout = view?.findViewById(R.id.message)!!
|
||||
val infoView: TextView = view?.findViewById(R.id.info)!!
|
||||
val textView: TextView = view?.findViewById(R.id.text)!!
|
||||
}
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
|
||||
val viewHolder: ViewHolder
|
||||
val rowView: View
|
||||
|
||||
if (view == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.message, parent, false)
|
||||
viewHolder = ViewHolder(rowView)
|
||||
rowView.tag = viewHolder
|
||||
} else {
|
||||
rowView = view
|
||||
viewHolder = rowView.tag as ViewHolder
|
||||
}
|
||||
|
||||
val listener = View.OnClickListener {
|
||||
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
val i = Intent(ctx, ContactActivity::class.java)
|
||||
val b = Bundle()
|
||||
b.putBoolean("new", true)
|
||||
b.putString("uri", peerUri)
|
||||
i.putExtras(b)
|
||||
ctx.startActivity(i)
|
||||
}
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
Message.messages().remove(rows[position])
|
||||
Message.save()
|
||||
rows.removeAt(position)
|
||||
this.notifyDataSetChanged()
|
||||
//if (rows.size == 0) {
|
||||
// listView.removeFooterView(footerView)
|
||||
//}
|
||||
}
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
val builder = MaterialAlertDialogBuilder(ctx, R.style.AlertDialogTheme)
|
||||
if (chatPeer == peerUri)
|
||||
with (builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(String.format(ctx.getString(R.string.long_message_question),
|
||||
peerUri))
|
||||
setNeutralButton(ctx.getString(R.string.cancel), dialogClickListener)
|
||||
setNegativeButton(ctx.getString(R.string.delete), dialogClickListener)
|
||||
setPositiveButton(ctx.getString(R.string.add_contact), dialogClickListener)
|
||||
show()
|
||||
}
|
||||
else
|
||||
with (builder) {
|
||||
setTitle(R.string.confirmation)
|
||||
setMessage(ctx.getText(R.string.short_message_question))
|
||||
setNeutralButton(ctx.getString(R.string.cancel), dialogClickListener)
|
||||
setNegativeButton(ctx.getString(R.string.delete), dialogClickListener)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
viewHolder.layoutView.setOnClickListener(listener)
|
||||
viewHolder.infoView.setOnClickListener(listener)
|
||||
viewHolder.textView.setOnClickListener(listener)
|
||||
|
||||
val message = rows[position]
|
||||
|
||||
val down = message.direction == MESSAGE_DOWN
|
||||
val lp = viewHolder.layoutView.layoutParams as LinearLayout.LayoutParams
|
||||
val peer: String = if (down) {
|
||||
lp.setMargins(0, 10, 75, 10)
|
||||
viewHolder.layoutView.setBackgroundResource(R.drawable.message_in_bg)
|
||||
if (chatPeer.startsWith("sip:") &&
|
||||
(Utils.uriHostPart(message.peerUri) == Utils.uriHostPart(message.aor)))
|
||||
Utils.uriUserPart(message.peerUri)
|
||||
else
|
||||
chatPeer
|
||||
} else {
|
||||
lp.setMargins(75, 10, 0, 10)
|
||||
viewHolder.layoutView.setBackgroundResource(R.drawable.message_out_bg)
|
||||
ctx.getString(R.string.you)
|
||||
}
|
||||
|
||||
viewHolder.layoutView.layoutParams = lp
|
||||
|
||||
var info: String
|
||||
val cal = GregorianCalendar()
|
||||
cal.timeInMillis = message.timeStamp
|
||||
val fmt: DateFormat = if (isToday(message.timeStamp))
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
else
|
||||
DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
info = fmt.format(cal.time)
|
||||
if (info.length < 6) info = "${ctx.getString(R.string.today)} $info"
|
||||
info = "$info - $peer"
|
||||
if (message.direction == MESSAGE_UP_FAIL) {
|
||||
info = if (message.responseCode != 0)
|
||||
"$info - ${ctx.getString(R.string.message_failed)}: " + "${message.responseCode} ${message.responseReason}"
|
||||
else
|
||||
"$info - ${ctx.getString(R.string.sending_failed)}"
|
||||
viewHolder.infoView.setTextColor(ContextCompat.getColor(ctx, R.color.colorAccent))
|
||||
}
|
||||
viewHolder.infoView.text = info
|
||||
|
||||
viewHolder.textView.text = message.message
|
||||
if (message.new)
|
||||
viewHolder.textView.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
return rowView
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import kotlin.math.abs
|
||||
|
||||
open class OnSwipeTouchListener(ctx: Context) : View.OnTouchListener {
|
||||
|
||||
private val gestureDetector: GestureDetector
|
||||
|
||||
companion object {
|
||||
private const val SWIPE_THRESHOLD = 100
|
||||
private const val SWIPE_VELOCITY_THRESHOLD = 100
|
||||
}
|
||||
|
||||
init {
|
||||
gestureDetector = GestureDetector(ctx, GestureListener())
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(view: View?, event: MotionEvent): Boolean {
|
||||
return try {
|
||||
gestureDetector.onTouchEvent(event)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "onTouch exception $e")
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
override fun onDown(event: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velX: Float, velY: Float): Boolean {
|
||||
var result = false
|
||||
try {
|
||||
val diffY = e2.y - e1!!.y
|
||||
val diffX = e2.x - e1.x
|
||||
if (abs(diffX) > abs(diffY)) {
|
||||
if (abs(diffX) > SWIPE_THRESHOLD && abs(velX) > SWIPE_VELOCITY_THRESHOLD) {
|
||||
if (diffX > 0) {
|
||||
onSwipeRight()
|
||||
} else {
|
||||
onSwipeLeft()
|
||||
}
|
||||
result = true
|
||||
}
|
||||
} else if (abs(diffY) > SWIPE_THRESHOLD && abs(velY) > SWIPE_VELOCITY_THRESHOLD) {
|
||||
result = if (diffY > 0) {
|
||||
onSwipeBottom()
|
||||
false
|
||||
} else {
|
||||
onSwipeTop()
|
||||
true
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "onFling exception $e")
|
||||
// e.printStackTrace()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
open fun onSwipeRight() {}
|
||||
|
||||
open fun onSwipeLeft() {}
|
||||
|
||||
open fun onSwipeTop() {}
|
||||
|
||||
open fun onSwipeBottom() {}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
|
||||
class UaSpinnerAdapter(cxt: Context, private val uas: ArrayList<UserAgent>) :
|
||||
ArrayAdapter<UserAgent>(cxt, android.R.layout.simple_spinner_item, uas) {
|
||||
|
||||
private val layoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private class ViewHolder(view: View?) {
|
||||
val textView: TextView = view?.findViewById(R.id.spinnerText)!!
|
||||
val imageView: ImageView = view?.findViewById(R.id.spinnerImage)!!
|
||||
}
|
||||
|
||||
override fun getDropDownView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
return getImageForPosition(position, view, parent)
|
||||
}
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
return getImageForPosition(position, view, parent)
|
||||
}
|
||||
|
||||
private fun getImageForPosition(position: Int, view: View?, parent: ViewGroup): View {
|
||||
|
||||
val viewHolder: ViewHolder
|
||||
val rowView: View
|
||||
|
||||
if (view == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.account_spinner, parent, false)
|
||||
viewHolder = ViewHolder(rowView)
|
||||
rowView.tag = viewHolder
|
||||
} else {
|
||||
rowView = view
|
||||
viewHolder = rowView.tag as ViewHolder
|
||||
}
|
||||
|
||||
val ua = uas[position]
|
||||
viewHolder.textView.text = if (ua.account.nickName != "")
|
||||
ua.account.nickName
|
||||
else
|
||||
ua.account.aor.split(":")[1]
|
||||
viewHolder.textView.textSize = 17f
|
||||
if (BaresipService.uas.size > 1 && ua.calls().isNotEmpty())
|
||||
viewHolder.textView.setTypeface(null, Typeface.BOLD)
|
||||
viewHolder.imageView.setImageResource(ua.status)
|
||||
|
||||
return rowView
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import com.tutpro.baresip.BaresipService.Companion.uas
|
||||
import com.tutpro.baresip.BaresipService.Companion.uasStatus
|
||||
|
||||
class UserAgent(val uap: Long) {
|
||||
|
||||
val account = Account(Api.ua_account(uap))
|
||||
@ -10,11 +13,22 @@ class UserAgent(val uap: Long) {
|
||||
}
|
||||
|
||||
fun add() {
|
||||
BaresipService.uas.add(this)
|
||||
val updatedUas = uas.value.toMutableList()
|
||||
updatedUas.add(this)
|
||||
uas.value = updatedUas.toList()
|
||||
uasStatus.value = statusMap()
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
BaresipService.uas.remove(this)
|
||||
val updatedUas = uas.value.toMutableList()
|
||||
updatedUas.remove(this)
|
||||
uas.value = updatedUas.toList()
|
||||
uasStatus.value = statusMap()
|
||||
}
|
||||
|
||||
fun uaUpdateStatus(status: Int) {
|
||||
uas.value.find { it.uap == this.uap }?.status = status
|
||||
uasStatus.value = statusMap()
|
||||
}
|
||||
|
||||
fun calls(dir: String = ""): ArrayList<Call> {
|
||||
@ -34,19 +48,19 @@ class UserAgent(val uap: Long) {
|
||||
companion object {
|
||||
|
||||
fun ofAor(aor: String): UserAgent? {
|
||||
for (ua in BaresipService.uas)
|
||||
for (ua in uas.value)
|
||||
if (ua.account.aor == aor) return ua
|
||||
return null
|
||||
}
|
||||
|
||||
fun ofDomain(domain: String): UserAgent? {
|
||||
for (ua in BaresipService.uas)
|
||||
for (ua in uas.value)
|
||||
if (Utils.aorDomain(ua.account.aor) == domain) return ua
|
||||
return null
|
||||
}
|
||||
|
||||
fun ofUap(uap: Long): UserAgent? {
|
||||
for (ua in BaresipService.uas)
|
||||
for (ua in uas.value)
|
||||
if (ua.uap == uap) return ua
|
||||
return null
|
||||
}
|
||||
@ -59,20 +73,27 @@ class UserAgent(val uap: Long) {
|
||||
}
|
||||
|
||||
fun findAorIndex(aor: String): Int? {
|
||||
for (i in BaresipService.uas.indices) {
|
||||
if (BaresipService.uas[i].account.aor == aor) return i
|
||||
for (i in uas.value.indices) {
|
||||
if (uas.value[i].account.aor == aor) return i
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun register() {
|
||||
for (ua in BaresipService.uas) {
|
||||
for (ua in uas.value) {
|
||||
if (ua.account.regint > 0) {
|
||||
if (Api.ua_register(ua.uap) != 0)
|
||||
Log.d(TAG, "Failed to register ${ua.account.aor}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun statusMap(): Map<String, Int> {
|
||||
val result = emptyMap<String, Int>().toMutableMap()
|
||||
for (ua in uas.value)
|
||||
result[ua.account.aor] = ua.status
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,44 +7,60 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Bitmap.createScaledBitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.media.AudioAttributes
|
||||
import android.net.Uri
|
||||
import android.net.wifi.WifiManager
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.media.audiofx.AcousticEchoCanceler
|
||||
import android.os.*
|
||||
import android.media.audiofx.AutomaticGainControl
|
||||
import android.net.Uri
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import java.io.*
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
import java.lang.reflect.Method
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
import java.security.SecureRandom
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
import java.util.Enumeration
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.Locale
|
||||
import java.util.Random
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
@ -54,6 +70,7 @@ import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import androidx.core.graphics.scale
|
||||
|
||||
object Utils {
|
||||
|
||||
@ -161,7 +178,7 @@ object Utils {
|
||||
return u
|
||||
}
|
||||
|
||||
fun e164Uri(uri: String, countryCode: String): String {
|
||||
private fun e164Uri(uri: String, countryCode: String): String {
|
||||
if (countryCode == "") return uri
|
||||
val scheme = uri.substring(0, 4)
|
||||
val userPart = uriUserPart(uri)
|
||||
@ -488,30 +505,6 @@ object Utils {
|
||||
return result
|
||||
}
|
||||
|
||||
fun dtmfWatcher(callp: Long): TextWatcher {
|
||||
return object : TextWatcher {
|
||||
override fun beforeTextChanged(sequence: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(sequence: CharSequence, start: Int, before: Int, count: Int) {
|
||||
val text = sequence.subSequence(start, start + count).toString()
|
||||
if (text.isNotEmpty()) {
|
||||
val digit = text[0]
|
||||
val call = Call.ofCallp(callp)
|
||||
if (call == null) {
|
||||
Log.w(TAG, "dtmfWatcher did not find call $callp")
|
||||
} else {
|
||||
Log.d(TAG, "Got DTMF digit '$digit'")
|
||||
if (((digit >= '0') && (digit <= '9')) || (digit == '*') || (digit == '#'))
|
||||
call.sendDigit(digit)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun afterTextChanged(sequence: Editable) {
|
||||
// KEYCODE_REL
|
||||
// call_send_digit(callp, 4.toChar())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun checkPermissions(ctx: Context, permissions: Array<String>) : Boolean {
|
||||
for (p in permissions) {
|
||||
if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) {
|
||||
@ -524,23 +517,6 @@ object Utils {
|
||||
return true
|
||||
}
|
||||
|
||||
fun View.showSnackBar(
|
||||
view: View,
|
||||
msg: String,
|
||||
length: Int,
|
||||
actionMessage: CharSequence?,
|
||||
action: (View) -> Unit
|
||||
) {
|
||||
val snackBar = Snackbar.make(view, msg, length)
|
||||
if (actionMessage != null) {
|
||||
snackBar.setAction(actionMessage) {
|
||||
action(this)
|
||||
}.show()
|
||||
} else {
|
||||
snackBar.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun copyAssetToFile(context: Context, asset: String, path: String) {
|
||||
try {
|
||||
val `is` = context.assets.open(asset)
|
||||
@ -649,7 +625,7 @@ object Utils {
|
||||
if (file.exists()) file.delete()
|
||||
try {
|
||||
val out = FileOutputStream(file)
|
||||
val scaledBitmap = createScaledBitmap(bitmap, 96, 96, true)
|
||||
val scaledBitmap = bitmap.scale(96, 96)
|
||||
scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
@ -663,7 +639,6 @@ object Utils {
|
||||
|
||||
class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray): Serializable {
|
||||
companion object {
|
||||
@Suppress("ConstPropertyName")
|
||||
private const val serialVersionUID: Long = -29238082928391L
|
||||
}
|
||||
}
|
||||
@ -862,7 +837,7 @@ object Utils {
|
||||
|
||||
fun randomColor(): Int {
|
||||
val rnd = Random()
|
||||
return Color.argb(255, rnd.nextInt(256), rnd.nextInt(256),
|
||||
return android.graphics.Color.argb(255, rnd.nextInt(256), rnd.nextInt(256),
|
||||
rnd.nextInt(256))
|
||||
}
|
||||
|
||||
@ -877,6 +852,19 @@ object Utils {
|
||||
|
||||
}
|
||||
|
||||
fun isThemeDark(ctx: Context) : Boolean {
|
||||
return Preferences(ctx).displayTheme == AppCompatDelegate.MODE_NIGHT_YES ||
|
||||
ctx.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) ==
|
||||
Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun isNightMode() = when (AppCompatDelegate.getDefaultNightMode()) {
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> false
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> true
|
||||
else -> isSystemInDarkTheme()
|
||||
}
|
||||
|
||||
fun relativeTime(ctx: Context, time: GregorianCalendar): String {
|
||||
return if (DateUtils.isToday(time.timeInMillis)) {
|
||||
val fmt = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
@ -894,8 +882,8 @@ object Utils {
|
||||
}
|
||||
}
|
||||
|
||||
fun bitmapFromView(view: View): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(view.layoutParams.width, view.layoutParams.height, Bitmap.Config.ARGB_8888)
|
||||
private fun bitmapFromView(view: View): Bitmap {
|
||||
val bitmap = createBitmap(view.layoutParams.width, view.layoutParams.height)
|
||||
val canvas = Canvas(bitmap)
|
||||
view.layout(0, 0, view.layoutParams.width, view.layoutParams.height)
|
||||
view.draw(canvas)
|
||||
@ -1044,32 +1032,41 @@ object Utils {
|
||||
}
|
||||
}
|
||||
|
||||
fun isDarkTheme(ctx: Context): Boolean {
|
||||
return ctx.resources.configuration.uiMode and
|
||||
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
fun aecAgcCheck() {
|
||||
|
||||
fun isAecSupported(): Boolean {
|
||||
if (!AcousticEchoCanceler.isAvailable()) {
|
||||
Log.i(TAG, "Hardware AEC is NOT available")
|
||||
return false
|
||||
}
|
||||
|
||||
val sessionId = Api.create_AAudio_SessionId()
|
||||
val sessionId = Api.AAudio_open_stream()
|
||||
if (sessionId == -1) {
|
||||
Log.e(TAG, "Failed to create AAudio stream for session ID")
|
||||
return false
|
||||
Log.e(TAG, "Failed to open AAudio stream")
|
||||
return
|
||||
}
|
||||
|
||||
val aec = AcousticEchoCanceler.create(sessionId)
|
||||
return if (aec != null) {
|
||||
aec.release()
|
||||
Log.i(TAG, "Hardware AEC is supported")
|
||||
true
|
||||
} else {
|
||||
Log.w(TAG, "Hardware AEC is NOT supported")
|
||||
false
|
||||
if (AcousticEchoCanceler.isAvailable()) {
|
||||
val aec = AcousticEchoCanceler.create(sessionId)
|
||||
if (aec != null) {
|
||||
BaresipService.aecAvailable = true
|
||||
aec.release()
|
||||
Log.d(TAG, "Creation of hardware AEC for $sessionId succeeded")
|
||||
} else {
|
||||
Log.w(TAG, "Creation of hardware AEC for $sessionId failed")
|
||||
}
|
||||
}
|
||||
else
|
||||
Log.i(TAG, "Hardware AEC is NOT available")
|
||||
|
||||
if (AutomaticGainControl.isAvailable()) {
|
||||
val agc = AcousticEchoCanceler.create(sessionId)
|
||||
if (agc != null) {
|
||||
BaresipService.agcAvailable = true
|
||||
agc.release()
|
||||
Log.d(TAG, "Creation of hardware AGC for $sessionId succeeded")
|
||||
} else {
|
||||
Log.w(TAG, "Creation of hardware AGC for $sessionId failed")
|
||||
}
|
||||
}
|
||||
else
|
||||
Log.i(TAG, "Hardware AGC is NOT available")
|
||||
|
||||
Api.AAudio_close_stream()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
17
app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt
Normal file
17
app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package com.tutpro.baresip
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class ViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val _selectedAor = MutableStateFlow("")
|
||||
val selectedAor: StateFlow<String> = _selectedAor.asStateFlow()
|
||||
|
||||
fun updateSelectedAor(newValue: String) {
|
||||
_selectedAor.value = newValue
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/colorSecondaryDark"/>
|
||||
<corners android:bottomLeftRadius="5dp" android:radius="15dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/colorPrimaryDark"/>
|
||||
<corners android:topRightRadius="5dp" android:radius="15dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item>
|
||||
<shape>
|
||||
<corners android:radius="10dp" />
|
||||
<solid android:color="@color/colorGray" />
|
||||
<!--<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/colorSecondaryDark" />-->
|
||||
</shape>
|
||||
</item>
|
||||
<item android:right="12dp">
|
||||
<bitmap
|
||||
android:gravity="end"
|
||||
android:src="@android:drawable/arrow_down_float" />
|
||||
</item>
|
||||
</layer-list>
|
||||
5
app/src/main/res/drawable/arrow_drop_down.xml
Normal file
5
app/src/main/res/drawable/arrow_drop_down.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="48dp" android:viewportHeight="24" android:viewportWidth="24" android:width="48dp">
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="m8.71,11.71l2.59,2.59c0.39,0.39 1.02,0.39 1.41,0l2.59,-2.59c0.63,-0.63 0.18,-1.71 -0.71,-1.71H9.41c-0.89,0 -1.33,1.08 -0.7,1.71"/>
|
||||
|
||||
</vector>
|
||||
@ -4,6 +4,6 @@
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960" >
|
||||
<path
|
||||
android:fillColor="@color/colorTrafficGreen"
|
||||
android:fillColor="@color/colorSecondary"
|
||||
android:pathData="M370,640L430,640L430,320L370,320L370,640ZM530,640L590,640L590,320L530,320L530,640ZM480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,397 111.5,324Q143,251 197.5,197Q252,143 325,111.5Q398,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,562 848.5,635Q817,708 763,762.5Q709,817 636,848.5Q563,880 480,880ZM480,820Q622,820 721,720.5Q820,621 820,480Q820,338 721,239Q622,140 480,140Q339,140 239.5,239Q140,338 140,480Q140,621 239.5,720.5Q339,820 480,820ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</vector>
|
||||
|
||||
@ -4,6 +4,6 @@
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960" >
|
||||
<path
|
||||
android:fillColor="@color/colorTrafficGreen"
|
||||
android:fillColor="@color/colorSecondary"
|
||||
android:pathData="M480,626L626,480L480,334L438,376L512,450L330,450L330,510L512,510L438,584L480,626ZM480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,397 111.5,324Q143,251 197.5,197Q252,143 325,111.5Q398,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,562 848.5,635Q817,708 763,762.5Q709,817 636,848.5Q563,880 480,880ZM480,820Q622,820 721,720.5Q820,621 820,480Q820,338 721,239Q622,140 480,140Q339,140 239.5,239Q140,338 140,480Q140,621 239.5,720.5Q339,820 480,820ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</vector>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<vector android:height="48dp" android:tint="@color/colorAccent"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white"
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
||||
</vector>
|
||||
|
||||
5
app/src/main/res/drawable/visibility.xml
Normal file
5
app/src/main/res/drawable/visibility.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/visibility_off.xml
Normal file
5
app/src/main/res/drawable/visibility_off.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
|
||||
|
||||
</vector>
|
||||
@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/accountRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@id/action"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginBottom="6dp">
|
||||
</TextView>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:icon="@android:drawable/ic_menu_delete"
|
||||
android:background="@null"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/delete">
|
||||
</ImageButton>
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TableRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="UselessParent"
|
||||
android:padding="11dp" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/spinnerImage"
|
||||
android:layout_marginStart="5dp"
|
||||
android:contentDescription="@string/status" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/spinnerText"
|
||||
android:textColor="@color/colorSpinnerText"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="0dp" />
|
||||
|
||||
</TableRow>
|
||||
|
||||
</TableLayout>
|
||||
@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".AboutActivity" >
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical"
|
||||
tools:ignore="UselessParent"
|
||||
android:fillViewport="true" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aboutText"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
@ -1,510 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/AccountView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:focusableInTouchMode="true" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/Uri"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textSize="18sp"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="" >
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/NickNameTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/account_nickname" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/NickName"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/nickname"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:inputType="textPersonName|textCapWords"
|
||||
android:textSize="18sp"
|
||||
android:autofillHints="name"
|
||||
android:layout_width="fill_parent">
|
||||
</EditText>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/DisplayNameTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/display_name" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/DisplayName"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/your_name"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:inputType="textPersonName|textCapWords"
|
||||
android:textSize="18sp"
|
||||
android:autofillHints="name"
|
||||
android:layout_width="fill_parent">
|
||||
</EditText>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/AuthUserTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/authentication_username" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/AuthUser"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/user_id"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:textSize="18sp"
|
||||
android:inputType="textEmailAddress"
|
||||
android:autofillHints="username"
|
||||
android:layout_width="fill_parent">
|
||||
</EditText>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/AuthPassTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/authentication_password" >
|
||||
</TextView>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:hintEnabled="false"
|
||||
app:endIconMode="password_toggle">
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/AuthPass"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1">
|
||||
</androidx.appcompat.widget.AppCompatEditText>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/OutboundProxyTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/outbound_proxies" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/Outbound1"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/sip_uri_of_proxy_server"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:textSize="18sp"
|
||||
android:inputType="textEmailAddress"
|
||||
android:importantForAutofill="no"
|
||||
android:layout_width="fill_parent" >
|
||||
</EditText>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/Outbound2"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:hint="@string/sip_uri_of_another_proxy_server"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:textSize="18sp"
|
||||
android:inputType="textEmailAddress"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/RegTitle"
|
||||
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" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/Register"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="end"
|
||||
android:layout_centerVertical="true"
|
||||
android:checked="false" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/RegIntTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/RegInt"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/reg_int" >
|
||||
</TextView>
|
||||
<EditText
|
||||
android:id="@+id/RegInt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="end"
|
||||
android:layout_centerVertical="true"
|
||||
android:textSize="18sp"
|
||||
android:inputType="number"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/AudioCodecsTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/audio_codecs" >
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/MediaEncTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="20dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/media_encryption" >
|
||||
</TextView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/mediaEncSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp" >
|
||||
</Spinner>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/MediaNatTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="12dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/media_nat" >
|
||||
</TextView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/mediaNatSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp" >
|
||||
</Spinner>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/Stun"
|
||||
android:layout_width="fill_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/StunServerTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/stun_server" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/StunServer"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:textSize="18sp"
|
||||
android:scrollHorizontally="true"
|
||||
android:importantForAutofill="no"
|
||||
android:hint="@string/stun_server_uri"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:inputType="textEmailAddress" >
|
||||
</EditText>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/StunUserTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/stun_username" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/StunUser"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:hint="@string/user_id"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:textSize="18sp"
|
||||
android:inputType="textEmailAddress"
|
||||
android:autofillHints="username" >
|
||||
</EditText>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/StunPassTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/stun_password" >
|
||||
</TextView>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="4dp"
|
||||
app:hintEnabled="false"
|
||||
app:endIconMode="password_toggle">
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/StunPass"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textPassword"
|
||||
android:hint="@string/password"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:maxLines="1">
|
||||
</androidx.appcompat.widget.AppCompatEditText>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/RtcpMuxTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/RtcpMux"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/rtcp_mux" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/RtcpMux"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="end"
|
||||
android:layout_centerVertical="true"
|
||||
android:checked="false" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="0dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/Rel100Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/Rel100"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/rel_100" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/Rel100"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="end"
|
||||
android:layout_centerVertical="true"
|
||||
android:checked="false" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/DtmfModeTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/dtmf_mode" >
|
||||
</TextView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/dtmfModeSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp" >
|
||||
</Spinner>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/AnswerModeTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="14dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/answer_mode" >
|
||||
</TextView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/answerModeSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp" >
|
||||
</Spinner>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/RedirectModeTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="14dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/redirect_mode" >
|
||||
</TextView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/redirectModeSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp" >
|
||||
</Spinner>
|
||||
<TextView
|
||||
android:id="@+id/VoicemailUriTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="14dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/voicemail_uri" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/voicemailUri"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/sip_uri"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:textSize="18sp"
|
||||
android:inputType="textUri"
|
||||
android:importantForAutofill="no"
|
||||
android:layout_width="fill_parent">
|
||||
</EditText>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/CountryCodeTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/countryCode"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/country_code" >
|
||||
</TextView>
|
||||
<EditText
|
||||
android:id="@+id/countryCode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="end"
|
||||
android:layout_centerVertical="true"
|
||||
android:hint="@string/country_code_hint"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:textSize="18sp"
|
||||
android:inputType="phone"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/TelephonyProviderTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/telephony_provider" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/telephonyProvider"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/telephony_provider_hint"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:textSize="18sp"
|
||||
android:inputType="textUri"
|
||||
android:importantForAutofill="no"
|
||||
android:layout_width="fill_parent">
|
||||
</EditText>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="10dp">
|
||||
<TextView
|
||||
android:id="@+id/DefaultTitle"
|
||||
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" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/Default"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/AccountsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="com.tutpro.baresip.AccountsActivity" >
|
||||
|
||||
<ListView
|
||||
android:id="@+id/accounts"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/newAor"
|
||||
android:transcriptMode="alwaysScroll" >
|
||||
</ListView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/newAor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:textSize="20sp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_toStartOf="@+id/addAccount"
|
||||
android:inputType="textEmailAddress"
|
||||
android:autofillHints="username"
|
||||
android:scrollHorizontally="true"
|
||||
android:hint="@string/user_domain"
|
||||
android:textColorHint="@color/colorSecondaryDark" >
|
||||
</EditText>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/addAccount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginStart="6dp"
|
||||
android:src="@drawable/plus"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/add"
|
||||
android:scaleType="centerCrop" >
|
||||
</ImageButton>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/ContactView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/TextAvatar"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:scaleType="centerInside"
|
||||
android:background="@drawable/circle"
|
||||
android:textSize="72sp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:gravity="center" >
|
||||
</TextView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/CardAvatar"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:elevation="12dp"
|
||||
app:cardCornerRadius="48dp" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ImageAvatar"
|
||||
android:contentDescription="@string/avatar_image"
|
||||
android:layout_height="96dp"
|
||||
android:layout_width="96dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop" >
|
||||
</ImageView>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/Name"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:textColor="@color/colorStrong"
|
||||
android:textSize="24sp"
|
||||
tools:ignore="LabelFor">
|
||||
</TextView>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/uris"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:divider="#00000000"
|
||||
android:dividerHeight="0dp" >
|
||||
</ListView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -1,264 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/AudioView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:focusableInTouchMode="true" >
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/VolumeTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/VolumeSpinner"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/default_call_volume" >
|
||||
</TextView>
|
||||
<Spinner
|
||||
android:id="@+id/VolumeSpinner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_gravity="end" >
|
||||
</Spinner>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp" >
|
||||
<TextView
|
||||
android:id="@+id/SpeakerPhoneTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/SpeakerPhone"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/speaker_phone" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/SpeakerPhone"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="vertical" >
|
||||
<TextView
|
||||
android:id="@+id/AudioModulesTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/audio_modules_title" >
|
||||
</TextView>
|
||||
<LinearLayout
|
||||
android:id="@+id/AudioModulesList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="24dp"
|
||||
android:orientation="vertical" >
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/OpusBitRateTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/opus_bit_rate" >
|
||||
</TextView>
|
||||
<EditText
|
||||
android:id="@+id/OpusBitRate"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@+id/OpusBitRateTitle"
|
||||
android:layout_marginStart="10dp"
|
||||
android:textSize="18sp"
|
||||
android:scrollHorizontally="true"
|
||||
android:inputType="numberDecimal"
|
||||
android:hint="@string/_28000"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/OpusPacketLossTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/opus_packet_loss" >
|
||||
</TextView>
|
||||
<EditText
|
||||
android:id="@+id/OpusPacketLoss"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@+id/OpusPacketLossTitle"
|
||||
android:layout_marginStart="10dp"
|
||||
android:textSize="18sp"
|
||||
android:scrollHorizontally="true"
|
||||
android:inputType="numberDecimal"
|
||||
android:hint="@string/_0"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/AudioDelayTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/audio_delay" >
|
||||
</TextView>
|
||||
<EditText
|
||||
android:id="@+id/AudioDelay"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@+id/AudioDelayTitle"
|
||||
android:layout_marginStart="10dp"
|
||||
android:textSize="18sp"
|
||||
android:scrollHorizontally="true"
|
||||
android:inputType="number"
|
||||
android:hint="@string/_0"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/AecLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp" >
|
||||
<TextView
|
||||
android:id="@+id/AecTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/Aec"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/aec" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/Aec"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/MicGainLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/MicGainTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/microphone_gain" >
|
||||
</TextView>
|
||||
<EditText
|
||||
android:id="@+id/MicGain"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@+id/MicGainTitle"
|
||||
android:layout_marginStart="10dp"
|
||||
android:textSize="18sp"
|
||||
android:scrollHorizontally="true"
|
||||
android:inputType="numberDecimal"
|
||||
android:hint="@string/_1.0"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/ToneCountryTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/ToneCountrySpinner"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/tone_country" >
|
||||
</TextView>
|
||||
<Spinner
|
||||
android:id="@+id/ToneCountrySpinner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_gravity="end" >
|
||||
</Spinner>
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@ -1,66 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/CallDetailsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
tools:context=".CallDetailsActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/peer"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center_horizontal" >
|
||||
</TextView>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/headings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/peer"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<TextView android:id="@+id/direction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textSize="16sp"
|
||||
android:text="@string/direction" >
|
||||
</TextView>
|
||||
|
||||
<TextView android:id="@+id/time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginStart="76dp"
|
||||
android:textSize="16sp"
|
||||
android:text="@string/time" >
|
||||
</TextView>
|
||||
|
||||
<TextView android:id="@+id/duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:textSize="16sp"
|
||||
android:text="@string/calls_duration">
|
||||
</TextView>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/calls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/headings"
|
||||
android:layout_marginTop="12dp"
|
||||
android:divider="#00000000"
|
||||
android:dividerHeight="10dp" >
|
||||
</ListView>
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/CallsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".CallsActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center_horizontal" >
|
||||
</TextView>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/calls"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_below="@id/account"
|
||||
android:divider="#00000000"
|
||||
android:dividerHeight="10dp" >
|
||||
</ListView>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/ChatView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".ChatActivity" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:gravity="center_horizontal" >
|
||||
</TextView>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/messages"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/account"
|
||||
android:transcriptMode="alwaysScroll"
|
||||
android:stackFromBottom="true"
|
||||
android:divider="#00000000"
|
||||
android:dividerHeight="10dp"
|
||||
android:layout_above="@id/text" >
|
||||
</ListView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/text"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_toStartOf="@id/sendButton"
|
||||
android:textSize="18sp"
|
||||
android:inputType="textMultiLine|textShortMessage|textCapSentences|textAutoComplete|textAutoCorrect"
|
||||
android:scrollHorizontally="false"
|
||||
android:autofillHints="text"
|
||||
android:hint="@string/new_message"
|
||||
android:textColorHint="@color/colorSecondaryDark" >
|
||||
</EditText>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/sendButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:paddingTop="10dp"
|
||||
android:src="@drawable/send"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/send" >
|
||||
</ImageButton>
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/ChatsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".ChatsActivity" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:gravity="center_horizontal" >
|
||||
</TextView>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/chats"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/account"
|
||||
android:layout_above="@id/peer"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:transcriptMode="alwaysScroll"
|
||||
android:stackFromBottom="true"
|
||||
android:divider="#00000000"
|
||||
android:dividerHeight="16dp" >
|
||||
</ListView>
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/peer"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_toStartOf="@+id/plusButton"
|
||||
android:textSize="18sp"
|
||||
android:inputType="textEmailAddress"
|
||||
android:hint="@string/new_chat_peer"
|
||||
android:textColorHint="@color/colorSecondaryDark" >
|
||||
</AutoCompleteTextView>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/plusButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:src="@drawable/plus"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/add" >
|
||||
</ImageButton>
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/CodecsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".CodecsActivity" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/CodecsTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="20sp"
|
||||
android:text="@string/audio_codecs" >
|
||||
</TextView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/CodecList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,384 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/ConfigView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:focusableInTouchMode="true" >
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/AutoStartTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/AutoStart"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/start_automatically" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/AutoStart"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ListenAddressTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/listen_address"
|
||||
android:textSize="18sp" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/ListenAddress"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-10dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:textSize="18sp"
|
||||
android:hint="@string/_0_0_0_0_5060"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:inputType="textUri"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/NetAfTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/address_family" >
|
||||
</TextView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/NetAfSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:paddingTop="8dp" >
|
||||
</Spinner>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/DnsServersTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dns_servers"
|
||||
android:textSize="18sp">
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/DnsServers"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-10dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:textSize="18sp"
|
||||
android:inputType="textUri"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/CertificateFileTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/CertificateFile"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/tls_certificate_file" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/CertificateFile"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/VerifyServerTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/VerifyServer"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/verify_server" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/VerifyServer"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/CAFileTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/CAFile"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/tls_ca_file" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/CAFile"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/UserAgentTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/user_agent"
|
||||
android:textSize="18sp" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/UserAgent"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-10dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:textSize="18sp"
|
||||
android:inputType="text"
|
||||
android:importantForAutofill="no" >
|
||||
</EditText>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/AudioSettingsTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="18dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/audio_settings" >
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ContactsTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/contacts" >
|
||||
</TextView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/contactsSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:paddingTop="8dp" >
|
||||
</Spinner>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/DarkThemeTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/DarkTheme"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/dark_theme" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/DarkTheme"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/Battery"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/BatteryOptimizationsTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/BatteryOptimizations"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/battery_optimizations" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/BatteryOptimizations"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/PhoneApp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/DefaultPhoneAppTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/DefaultPhoneApp"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/default_phone_app" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/DefaultPhoneApp"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/DebugTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/Debug"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/debug" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/Debug"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/SipTraceTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/SipTrace"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/sip_trace" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/SipTrace"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/ResetTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/Reset"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/reset_config" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/Reset"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@ -1,139 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/ContactView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/TextAvatar"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:scaleType="centerInside"
|
||||
android:background="@drawable/circle"
|
||||
android:textSize="72sp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:gravity="center" >
|
||||
</TextView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/CardAvatar"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:elevation="12dp"
|
||||
app:cardCornerRadius="48dp" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ImageAvatar"
|
||||
android:contentDescription="@string/avatar_image"
|
||||
android:layout_height="96dp"
|
||||
android:layout_width="96dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop" >
|
||||
</ImageView>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/NameTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:paddingTop="10dp"
|
||||
android:text="@string/contact_name" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/Name"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName|textCapWords"
|
||||
android:textSize="18sp"
|
||||
android:layout_width="fill_parent"
|
||||
android:importantForAutofill="no"
|
||||
tools:ignore="LabelFor">
|
||||
</EditText>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/SipOrTelUriTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:paddingTop="10dp"
|
||||
android:text="@string/sip_or_tel_uri" >
|
||||
</TextView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/Uri"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/user_domain_or_number"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:inputType="textEmailAddress"
|
||||
android:textSize="18sp"
|
||||
android:importantForAutofill="no"
|
||||
tools:ignore="TextFields" >
|
||||
</EditText>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/FavoriteTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/Favorite"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/favorite" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/Favorite"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="5dp"
|
||||
android:orientation="horizontal" >
|
||||
<TextView
|
||||
android:id="@+id/AndroidTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/Android"
|
||||
android:textSize="18sp"
|
||||
android:text="@string/android" >
|
||||
</TextView>
|
||||
<CheckBox
|
||||
android:id="@+id/Android"
|
||||
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" >
|
||||
</CheckBox>
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/ContactsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".ContactsActivity">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/contacts"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/plusButton"
|
||||
android:divider="#00000000"
|
||||
android:dividerHeight="10dp" >
|
||||
</ListView>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/plusButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/plus"
|
||||
android:background="@null"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:contentDescription="@string/add" >
|
||||
</ImageButton>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
|
||||
@ -1,403 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
app:theme="@style/ActionBar">
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:layout_below="@id/toolbar" >
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/mainActivityLayout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/aorSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/spinner_bg"
|
||||
android:popupBackground="@color/colorSpinnerDropdown"
|
||||
android:gravity="center_vertical"
|
||||
android:spinnerMode="dropdown"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/callTitleRow"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/aorSpinner"
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/callTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/outgoing_call_to_dots"
|
||||
android:textSize="20sp">
|
||||
</TextView>
|
||||
|
||||
<Chronometer
|
||||
android:id="@+id/callTimer"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="3dp"
|
||||
android:gravity="end"
|
||||
android:visibility="invisible"
|
||||
android:textSize="18sp">
|
||||
</Chronometer>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/uriRow"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/callTitleRow"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="32dp"
|
||||
app:endIconMode="clear_text">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/callUri"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="32dp"
|
||||
android:completionThreshold="3"
|
||||
android:inputType="textEmailAddress"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:textSize="20sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/securityButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:visibility="invisible"
|
||||
android:background="#00000000"
|
||||
android:contentDescription="@string/app_name" >
|
||||
</ImageButton>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/diverter"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/uriRow"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/diverted_by_dots"
|
||||
android:textSize="20sp">
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/diverterUri"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:textColor="@color/colorStrong"
|
||||
android:textSize="20sp"
|
||||
android:layout_weight="1" >
|
||||
</TextView>
|
||||
|
||||
<View
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="1.5dp"
|
||||
android:layout_marginStart="3dp"
|
||||
android:layout_marginEnd="28dp"
|
||||
android:background="@color/colorGray"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/callRow"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/diverter"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="10dp" >
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/callButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="0dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/call"
|
||||
android:contentDescription="@string/call" >
|
||||
</ImageButton>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/hangupButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="0dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/hangup"
|
||||
android:clickable="false"
|
||||
android:visibility="invisible"
|
||||
android:contentDescription="@string/hangup" >
|
||||
</ImageButton>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/answerButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="0dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/call"
|
||||
android:clickable="false"
|
||||
android:visibility="invisible"
|
||||
android:contentDescription="@string/answer" >
|
||||
</ImageButton>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/rejectButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="0dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/hangup"
|
||||
android:clickable="false"
|
||||
android:visibility="invisible"
|
||||
android:contentDescription="@string/reject" >
|
||||
</ImageButton>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="70dp"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="invisible"
|
||||
android:id="@+id/callControl" >
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/holdButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="0dp"
|
||||
android:src="@drawable/call_hold"
|
||||
android:background="@null"
|
||||
android:clickable="false"
|
||||
android:contentDescription="@string/hold" >
|
||||
</ImageButton>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/transferButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="0dp"
|
||||
android:src="@drawable/call_transfer"
|
||||
android:background="@null"
|
||||
android:layout_marginStart="60dp"
|
||||
android:clickable="false"
|
||||
android:contentDescription="@string/call_transfer" >
|
||||
</ImageButton>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/dtmf"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="120dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:singleLine="true"
|
||||
android:scrollHorizontally="true"
|
||||
android:hint="@string/dtmf"
|
||||
android:textColorHint="@color/colorSecondaryDark"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="phone"
|
||||
android:digits="0123456789#*"
|
||||
android:textSize="20sp" >
|
||||
</EditText>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:src="@drawable/info"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="@null"
|
||||
android:clickable="false"
|
||||
android:contentDescription="@string/call_info" >
|
||||
</ImageButton>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/onHoldNotice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/buttons"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/colorAccent"
|
||||
android:text="@string/call_is_on_hold" >
|
||||
</TextView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/buttons"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:paddingBottom="6dp"
|
||||
android:layout_alignParentBottom="true" >
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" >
|
||||
</Space>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/voicemailButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:src="@drawable/voicemail"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/voicemail" >
|
||||
</ImageButton>
|
||||
|
||||
<Space
|
||||
android:id="@+id/voicemailButtonSpace"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1"
|
||||
android:visibility="gone" >
|
||||
</Space>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/contactsButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:src="@drawable/contacts"
|
||||
android:contentDescription="@string/contacts" >
|
||||
</ImageButton>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" >
|
||||
</Space>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/messagesButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:src="@drawable/messages"
|
||||
android:contentDescription="@string/messages" >
|
||||
</ImageButton>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" >
|
||||
</Space>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/callsButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:src="@drawable/calls"
|
||||
android:contentDescription="@string/call_history" >
|
||||
</ImageButton>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" >
|
||||
</Space>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/dialpadButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:src="@drawable/dialpad_off"
|
||||
android:contentDescription="@string/dialpad" >
|
||||
</ImageButton>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" >
|
||||
</Space>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="?dialogPreferredPadding"
|
||||
android:textSize="20sp"
|
||||
android:background="@color/colorPrimary"
|
||||
android:textColor="@color/colorWhite" >
|
||||
</TextView>
|
||||
@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/uriRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/uri"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:textSize="18sp"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp" >
|
||||
</TextView>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:src="@drawable/message"
|
||||
android:background="@null"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/call" >
|
||||
</ImageButton>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/call"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:src="@drawable/call_small"
|
||||
android:background="@null"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/call" >
|
||||
</ImageButton>
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/direction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginStart="12dp"
|
||||
android:contentDescription="@string/direction" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginStart="76dp"
|
||||
android:text="" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="" />
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,96 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/TextAvatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:scaleType="centerInside"
|
||||
android:background="@drawable/circle"
|
||||
android:textSize="24sp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" >
|
||||
</TextView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/CardAvatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:elevation="12dp"
|
||||
app:cardCornerRadius="18dp" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ImageAvatar"
|
||||
android:contentDescription="@string/avatar_image"
|
||||
android:layout_height="36dp"
|
||||
android:layout_width="36dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop" >
|
||||
</ImageView>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/directions"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="41dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:paddingBottom="5dp" >
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:focusable="false"
|
||||
android:gravity="end"
|
||||
android:textAlignment="gravity"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:textSize="12sp"
|
||||
android:lines="2"
|
||||
android:maxLines="2"
|
||||
android:text="" >
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/etc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/directions"
|
||||
android:paddingTop="10dp"
|
||||
android:text="" >
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/peer_uri"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/etc"
|
||||
android:layout_toStartOf="@id/time"
|
||||
android:focusable="false"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:textAlignment="gravity"
|
||||
android:textSize="18sp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:text="" >
|
||||
</TextView>
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,97 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="?dialogPreferredPadding"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:paddingBottom="8dp"
|
||||
android:textSize="20sp"
|
||||
android:text="@string/call_transfer"
|
||||
android:textColor="@color/colorPrimary" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/blindOrAttended"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:visibility="gone"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/blindTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:textSize="16sp"
|
||||
android:text="@string/blind" >
|
||||
</TextView>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/blind"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@id/blindTitle"
|
||||
android:checked="true" >
|
||||
</CheckBox>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/attendedTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/attended"
|
||||
android:textSize="16sp"
|
||||
android:text="@string/attended" >
|
||||
</TextView>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/attended"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:checked="false" >
|
||||
</CheckBox>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transferDestination"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingBottom="12dp"
|
||||
android:textSize="16sp"
|
||||
android:text="@string/transfer_destination"
|
||||
android:textColor="@color/colorStrong" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingEnd="?dialogPreferredPadding" >
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/transferUri"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAutofill="no"
|
||||
android:textSize="18sp"
|
||||
android:imeOptions="actionDone"
|
||||
android:labelFor="@id/transferUri"
|
||||
android:inputType="textEmailAddress" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@ -1,93 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/TextAvatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:scaleType="centerInside"
|
||||
android:background="@drawable/circle"
|
||||
android:textSize="24sp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" >
|
||||
</TextView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/CardAvatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:elevation="12dp"
|
||||
app:cardCornerRadius="18dp" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ImageAvatar"
|
||||
android:contentDescription="@string/avatar_image"
|
||||
android:layout_height="36dp"
|
||||
android:layout_width="36dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop" >
|
||||
</ImageView>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/chat"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="42dp" >
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/peer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:textSize="18sp"
|
||||
android:text="" >
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/info"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="end"
|
||||
android:textSize="12sp"
|
||||
android:text="" >
|
||||
</TextView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:textSize="16sp"
|
||||
android:singleLine="true"
|
||||
android:text="" >
|
||||
</TextView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp" >
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:layout_margin="4dp" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/codecName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/colorCodec"
|
||||
android:textSize="18sp"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/reorderImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:srcCompat="@drawable/reorder"
|
||||
android:contentDescription="@string/codec_action" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@ -1,66 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/contactRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/TextAvatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:scaleType="centerInside"
|
||||
android:background="@drawable/circle"
|
||||
android:textSize="24sp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" >
|
||||
</TextView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/CardAvatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:elevation="12dp"
|
||||
app:cardCornerRadius="18dp" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ImageAvatar"
|
||||
android:layout_height="36dp"
|
||||
android:layout_width="36dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/avatar_image">
|
||||
</ImageView>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contactName"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="41dp"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginBottom="6dp">
|
||||
</TextView>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:src="@drawable/edit"
|
||||
android:background="@null"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/edit" >
|
||||
</ImageButton>
|
||||
|
||||
</RelativeLayout>
|
||||
@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/message"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:ignore="UselessParent" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:textSize="12sp"
|
||||
android:text="" >
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:textSize="16sp"
|
||||
android:textIsSelectable="true"
|
||||
android:text="" >
|
||||
</TextView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@ -131,7 +131,11 @@
|
||||
<string name="start_automatically">Стартирайте автоматично</string>
|
||||
<string name="start_automatically_help">Ако е отметнато, baresip се стартира автоматично след включване на устройството.</string>
|
||||
<string name="listen_address">Слушане на адрес</string>
|
||||
<string name="listen_address_help">IP адрес и порт на формата \'адрес: порт\', на който baresip слуша за входящи SIP заявки. Ако IP адресът е IPv6 адрес, той трябва да бъде написан вътре в скоби []. IPv4 адрес 0.0.0.0 или IPv6 адрес [::] позволява на baresip да слуша всички налични адреси. Ако се остави празно (фабрично по подразбиране), baresip ще слуша на порт 5060 от всички налични адреси.</string>
|
||||
<string name="listen_address_help">IP адрес и порт на формата \'адрес: порт\', на който baresip слуша
|
||||
за входящи SIP заявки. Ако IP адресът е IPv6 адрес, той трябва да бъде написан вътре в скоби [].
|
||||
IPv4 адрес 0.0.0.0 или IPv6 адрес [::] позволява на baresip да слуша всички налични адреси.
|
||||
Ако се остави празно (фабрично по подразбиране), baresip ще слуша на порт 5060 от всички налични адреси.
|
||||
</string>
|
||||
<string name="invalid_listen_address">Невалиден адрес за слушане</string>
|
||||
<string name="dns_servers">DNS сървъри</string>
|
||||
<string name="dns_servers_help">Списък разделен със запетая на адреси на DNS сървъри. Ако не е зададено,
|
||||
@ -147,7 +151,6 @@
|
||||
<string name="tls_ca_file">TLS CA File</string>
|
||||
<string name="tls_ca_file_help">Ако е поставено отметка, файл \'ca_certs.crt\' съдържащи TLS сертификати на Сертифициращите органи е бил или ще бъде зареден от директорията за Изтегляне.</string>
|
||||
<string name="aec">Акустично отменяне на ехото</string>
|
||||
<string name="aec_help">Ако е поставена отметка, се прави опит за отмяна на ехо при аудио повикване.</string>
|
||||
<string name="opus_bit_rate">Opus Bit Rate</string>
|
||||
<string name="opus_bit_rate_help">Средна максимална битова скорост, използвана от аудио поток на Opus.
|
||||
Валидните стойности са 6000-510000. Фабрично по подразбиране е 28000.
|
||||
@ -254,4 +257,4 @@
|
||||
<string name="no_calls">Разговори не могат да бъдат осъществени без разрешен достъп до микрофона.</string>
|
||||
<string name="accounts_help">Порта и транспортния протокол на акаунта могат да бъдат посочени по избор, когато се създаде нов акаунт: потребителско име@домейн[:port] [;transport=udp|tcp|tls]. Ако е даден порт и транспортният протокол не е даден, транспортният протокол по подразбиране е udp. Ако порт не е даден и транспортният протокол е даден, портът по подразбиране е 5060 или 5061 (tls). Ако нито едното не е посочено и не е посочен изходящ прокси, регистраторът на акаунта (ако има такъв) се определя единствено въз основа на DNS информация от домейна.</string>
|
||||
<string name="new_account">Нов Акаунт</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@ -237,7 +237,6 @@
|
||||
<string name="calls_call">volání</string>
|
||||
<string name="tls_ca_file_help">Pokud je zaškrtnuto, byl nebo bude načten soubor obsahující certifikáty TLS takových certifikačních autorit, které nejsou součástí operačního systému Android. Ve verzi systému Android 9 se ze složky Download načte soubor s názvem \'ca_certs.crt\'.</string>
|
||||
<string name="audio_settings">Nastavení zvuku</string>
|
||||
<string name="aec_extended_filter_help">Pokud je zaškrtnuto, používá se rozšířený filtr pro potlačení ozvěny.</string>
|
||||
<string name="opus_bit_rate_help">Průměrná maximální přenosová rychlost používaná zvukovým tokem Opus. Platné hodnoty jsou 6000-510000. Výchozí hodnota je 28000.</string>
|
||||
<string name="opus_packet_loss">Očekávaná ztráta paketů Opus</string>
|
||||
<string name="opus_packet_loss_help">Očekávaná procentuální ztráta paketů zvukového toku Opus od 0 do 100. Výchozí hodnota je 1. Hodnota 0 také vypíná funkci Opus Forward Error Correction (FEC).</string>
|
||||
@ -246,9 +245,7 @@
|
||||
<string name="default_call_volume">Výchozí hlasitost volání</string>
|
||||
<string name="dark_theme_help">Vynutit tmavé téma</string>
|
||||
<string name="failed_to_load_module">Nepodařilo se načíst modul.</string>
|
||||
<string name="aec_help">Pokud je zaškrtnuto, dojde u hovoru k pokusu o potlačení ozvěny.</string>
|
||||
<string name="audio_modules_help">Zvukové kodeky poskytované zaškrtnutými moduly jsou k dispozici pro použití v účtech.</string>
|
||||
<string name="aec_extended_filter">Rozšířený filtr AEC</string>
|
||||
<string name="invalid_opus_packet_loss">Neplatné procento ztráty paketů Opus</string>
|
||||
<string name="invalid_opus_bitrate">Neplatný datový tok Opus</string>
|
||||
<string name="default_call_volume_help">Pokud je nastaveno, je výchozí hlasitost zvuku hovoru na stupnici 1-10.</string>
|
||||
|
||||
@ -359,7 +359,6 @@
|
||||
<string name="video_codecs">Video Codecs</string>
|
||||
<string name="audio_settings">Audioeinstellungen</string>
|
||||
<string name="backup">Sicherungskopie</string>
|
||||
<string name="aec_help">Falls angekreuzt, wird versucht, Echounterdrückung auf den Anruf anzuwenden.</string>
|
||||
<string name="rtcp_mux">RTCP Multiplexverfahren</string>
|
||||
<string name="invalid_opus_packet_loss">Ungültiger Opus Paketverlustprozentsatz</string>
|
||||
<string name="default_call_volume">Standardmäßige Anruflautstärke</string>
|
||||
@ -381,8 +380,6 @@
|
||||
<string name="speaker_phone_help">Falls angekreuzt, wird die Lautsprecherfunktion automatisch aktiviert, wenn der Anruf beginnt.</string>
|
||||
<string name="audio_modules_title">Audiomodule</string>
|
||||
<string name="audio_modules_help">Audio Codecs, die von den ausgewählten Modulen bereitgestellt werden, sind für die Benutzer verfügbar.</string>
|
||||
<string name="aec_extended_filter">AEC Erweiterter Filter</string>
|
||||
<string name="aec_extended_filter_help">Falls angekreuzt, nutzt die Echounterdrückung den erweiterten Filter.</string>
|
||||
<string name="opus_bit_rate">Opus Bitrate</string>
|
||||
<string name="opus_bit_rate_help">Durchschnittliche maximale Bitrate, die vom Opus Audiostream genutzt wird. Gültige Werte sind 6000-510000. Die Werkseinstellung beträgt 28000.</string>
|
||||
<string name="opus_packet_loss">Erwarteter Paketverlust von Opus</string>
|
||||
|
||||
@ -139,7 +139,12 @@
|
||||
<string name="start_automatically">Comenzar automáticamente</string>
|
||||
<string name="start_automatically_help">Si está marcado, baresip se inicia automáticamente después de que el dispositivo se (re)inicia.</string>
|
||||
<string name="listen_address">Dirección de escucha</string>
|
||||
<string name="listen_address_help">Dirección IP y puerto de formulario \'address:port\' en el que escucha baresip para solicitudes SIP entrantes. Si la dirección IP es una dirección IPv6, debe escribirse dentro soportes []. La dirección IPv4 0.0.0.0 o la dirección IPv6 [::] hace que la escucha de baresip sea Direcciones disponibles. Si se deja vacío (predeterminado de fábrica), baresip escucha en el puerto 5060 de todas las direcciones disponibles.</string>
|
||||
<string name="listen_address_help">Dirección IP y puerto de formulario \'address:port\' en el que escucha baresip
|
||||
para solicitudes SIP entrantes. Si la dirección IP es una dirección IPv6, debe escribirse dentro
|
||||
soportes []. La dirección IPv4 0.0.0.0 o la dirección IPv6 [::] hace que la escucha de baresip sea
|
||||
Direcciones disponibles. Si se deja vacío (predeterminado de fábrica), baresip escucha en el puerto 5060 de
|
||||
todas las direcciones disponibles.
|
||||
</string>
|
||||
<string name="invalid_listen_address">Dirección de escucha no válida</string>
|
||||
<string name="dns_servers">Servidores DNS</string>
|
||||
<string name="dns_servers_help">Lista separada por comas de direcciones de servidores DNS. Si no se da,
|
||||
@ -155,7 +160,6 @@
|
||||
<string name="tls_ca_file">Archivo de CA de TLS</string>
|
||||
<string name="tls_ca_file_help">Si está marcada, se ha cargado o se cargará un archivo que contiene certificados TLS de dichas Autoridades de Certificación que no están incluidas en el SO Android. En las versión de Android 9, se carga un archivo llamado \'ca_certs.crt\' desde la carpeta Download.</string>
|
||||
<string name="aec">Cancelación de eco acústico</string>
|
||||
<string name="aec_help">Si está marcada, se intenta la cancelación del eco en el audio de la llamada.</string>
|
||||
<string name="opus_bit_rate">Tasa de bits de Opus</string>
|
||||
<string name="opus_bit_rate_help">Velocidad de bits máxima promedio utilizada por la transmisión de audio Opus.
|
||||
Los valores válidos son 6000-510000. El valor predeterminado de fábrica es 28000.</string>
|
||||
@ -284,8 +288,6 @@
|
||||
<string name="video_size">Tamaño del fotograma de vídeo</string>
|
||||
<string name="dark_theme_help">Forzar el tema de la pantalla oscura</string>
|
||||
<string name="dark_theme">Tema oscuro</string>
|
||||
<string name="aec_extended_filter_help">Si está marcada, la cancelación de eco está utilizando el filtro extendido.</string>
|
||||
<string name="aec_extended_filter">Filtro extendido AEC</string>
|
||||
<string name="verify_server_help">Si está marcada, baresip verifica los certificados TLS del Agente de Usuario SIP y de los Servidores Proxy SIP cuando se utiliza el transporte TLS.</string>
|
||||
<string name="verify_server">Verificar los certificados del servidor</string>
|
||||
<string name="transfer_request_to">Solicitud de transferencia de llamada a</string>
|
||||
@ -451,4 +453,4 @@ También se pueden comunicar problemas.
|
||||
<string name="microphone_gain_help">Multiplica el volumen del micrófono por este número decimal. El valor mínimo es 1.0 (predeterminado de fábrica) que desactiva la ganancia del micrófono. Los valores más altos pueden afectar negativamente a la calidad del audio.</string>
|
||||
<string name="microphone_gain">Ganancia del micrófono</string>
|
||||
<string name="invalid_microphone_gain">Valor de ganancia de micrófono no válido</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@ -6,8 +6,10 @@
|
||||
<string name="about_text">
|
||||
<![CDATA[
|
||||
<h1>Baresip-kirjastoon perustuva, äänipuhelut ja pikaviestit mahdollistava SIP-sovellus</h1>
|
||||
<br>
|
||||
<p>Juha Heinanen <jh@tutpro.com></p>
|
||||
<p>Versio %1$s</p>
|
||||
<br>
|
||||
<h2>Käyttövihjeitä</h2>
|
||||
<ul>
|
||||
<li>Tarkista, että baresip-sovelluksen Asetukset vastaavat tarpeitasi. Kunkin otsikon kosketus tarjoaa apua.</li>
|
||||
@ -17,7 +19,6 @@
|
||||
valkoinen (rekisteröintiä ei ole aktivoitu).</li> <li>Pisteen kosketus johtaa suoraan tilin asetuksiin.</li>
|
||||
<li>Pyyhkäisy alas aikaansaa parhaillaan näkyvissä olevan tilin uudelleen rekisteröinnin.</li>
|
||||
<li>Parhaillaan näkyvissä olevan tilin pitkä kosketus aktivoi tai passivoi tilin rekisteröinnin.</li>
|
||||
<li>Pyyhkäisyllä vasemmalle/oikealle voit vaihtaa näkyvissä olevaa tiliä.</li>
|
||||
<li>Voit valita edellisen puhelun uudelleen koskettamalla virheää luuria silloin, kun puhelun kohde on tyhjä.</li>
|
||||
<li>Voit lisätä puheluiden ja viestien kohteet yhteystietoihin pitkällä kosketuksella.</li>
|
||||
<li>Pitkillä kosketuksilla voit myös poistaa puheluita, viestiketjuja, viestejä ja yhteystietoja.</li>
|
||||
@ -25,10 +26,12 @@
|
||||
<li>Katso lisätietoja <a href=https://github.com/juha-h/baresip-studio/wiki>Wiki</a>-sivulta.</li>
|
||||
</ul>
|
||||
<h2>Tietosuoja</h2>
|
||||
Sovelluksen tietosuojakäytäntö on luettavissa <a href="https://raw.githubusercontent.com/juha-h/baresip-studio/master/PrivacyPolicy.txt">täältä</a>.
|
||||
<p>Sovelluksen tietosuojakäytäntö on luettavissa <a href="https://raw.githubusercontent.com/juha-h/baresip-studio/master/PrivacyPolicy.txt">täältä</a>.</p>
|
||||
<br>
|
||||
<h2>Lähdekoodi</h2>
|
||||
Lähdekoodi on saatavilla <a href=https://github.com/juha-h/baresip-studio>GitHub</a>:ssa,
|
||||
missä voi myös raportoida virheistä.
|
||||
<p>Lähdekoodi on saatavilla <a href=https://github.com/juha-h/baresip-studio>GitHub</a>:ssa,
|
||||
missä voi myös raportoida virheistä.</p>
|
||||
<br>
|
||||
<h2>Lisenssit</h2>
|
||||
<ul>
|
||||
<li><b>BSD-3-Clause</b> except the following:</li>
|
||||
@ -223,9 +226,9 @@
|
||||
</string>
|
||||
<!-- Accounts Activity -->
|
||||
<string name="accounts">Tilit</string>
|
||||
<string name="new_account">Uusi tili</string>
|
||||
<string name="accounts_help">Kun uusi tili luodaan, voidaan haluttaessa antaa myös porttinumero
|
||||
ja tiedonsiirtoprotokolla: <käyttäjä>@<domain>[:<portti>][;transport=udp|tcp|tls].
|
||||
<string name="new_account">Uuden tilin SIP URI</string>
|
||||
<string name="accounts_help">Uuden tilin SIP URI muotoa
|
||||
<käyttäjä>@<domain>[:<portti>][;transport=udp|tcp|tls].
|
||||
Jos <portti> on annettu, mutta protokollaa ei ole annettu, protokolla on udp.
|
||||
Jos <portti> ei ole annettu, mutta protokolla on annettu, portti on joko 5060 tai 5061 (tls).
|
||||
Jos kumpaakaan ei ole annettu eikä välityspalvelinta ole määritelty, tilin mahdollinen
|
||||
@ -293,11 +296,11 @@
|
||||
<!-- Codecs Activity -->
|
||||
<string name="audio_codecs">Audion koodausmenetelmät</string>
|
||||
<string name="audio_codecs_help">Luettelo audion koodausmenetelmistä prioriteettijärjestyksessä.
|
||||
Järjestä raahaamalla ja pudottamalla, ota käyttöön tai poista käytöstä pyyhkäisemällä oikealle.
|
||||
Järjestä raahaamalla ja pudottamalla, ota käyttöön tai poista käytöstä pitkällä painalluksella.
|
||||
</string>
|
||||
<string name="video_codecs">Videon koodausmenetelmät</string>
|
||||
<string name="video_codecs_help">Luettelo videon koodausmenetelmistä prioriteettijärjestyksessä.
|
||||
Järjestä raahaamalla ja pudottamalla, ota käyttöön tai poista käytöstä pyyhkäisemällä oikealle.
|
||||
Järjestä raahaamalla ja pudottamalla, ota käyttöön tai poista käytöstä pitkällä painalluksella.
|
||||
</string>
|
||||
<string name="codec_action">Järjestä</string>
|
||||
<!-- Config Activity -->
|
||||
@ -365,9 +368,6 @@
|
||||
</string>
|
||||
<string name="failed_to_load_module">Modulin lataaminen epäonnistui.</string>
|
||||
<string name="aec">Akustinen kaiun poisto (AKP)</string>
|
||||
<string name="aec_help">Jos merkitty, kaikua yritetään poistaa ohjelmiston avulla.</string>
|
||||
<string name="no_hw_aec">Laitteellasi ei voida poistaa kaikua laitteiston avulla.</string>
|
||||
<string name="aec_extended_filter">AKP laajennettu suodatin</string>
|
||||
<string name="microphone_gain">Mikrofonin äänikerroin</string>
|
||||
<string name="microphone_gain_help">Kertoo mikrofonin äänen voimakkuuden tällä desimaaliluvulla.
|
||||
Minimikerroin on 1.0 (tehdasasetus), mikä ei muuta mikrofonin äänen voimakkuutta. Suuremmat
|
||||
@ -433,6 +433,8 @@
|
||||
<string name="sip_or_tel_uri">SIP tai tel URI</string>
|
||||
<string name="user_domain_or_number">käyttäjä@domain tai puhelinnumero</string>
|
||||
<string name="favorite">Suosikki</string>
|
||||
<string name="favorite_help">If ruksattu, kontakti näytetään muiden suosikkien
|
||||
kanssa yhteystietolistan alussa</string>
|
||||
<string name="invalid_contact">Virheellinen yhteystiedon nimi \'%1$s\'</string>
|
||||
<string name="contact_already_exists">Yhteystieto \'%1$s\' on jo olemassa.</string>
|
||||
<string name="invalid_contact_uri">Virheellinen SIP:n URI</string>
|
||||
@ -470,6 +472,8 @@
|
||||
<string name="invalid_sip_or_tel_uri">Virheellinen SIP tai puh. URI \'%1$s\'</string>
|
||||
<string name="baresip" translatable="false">baresip</string>
|
||||
<string name="android" translatable="false">Android</string>
|
||||
<string name="hardware">Laitteisto</string>
|
||||
<string name="software">Ohjelmisto</string>
|
||||
<!-- Main Activity -->
|
||||
<string name="backup">Tallenna</string>
|
||||
<string name="restore">Palauta</string>
|
||||
|
||||
@ -130,7 +130,12 @@
|
||||
<string name="calls_add_delete_question">Do you want to add \'%1$s\' to contacts or delete %2$s from call history?
|
||||
</string>
|
||||
<string name="long_chat_question">Do you want to delete chat with peer \'%1$s\' or add peer to contacts?</string>
|
||||
<string name="listen_address_help">IP address and port of form \'address:port\' at which baresip listens for incoming SIP requests. If IP address is an IPv6 address, it must be written inside brackets []. IPv4 address 0.0.0.0 or IPv6 address [::] makes baresip listen at all available addresses. If left empty (factory default), baresip listens at port 5060 of all available addresses.</string>
|
||||
<string name="listen_address_help">IP address and port of form \'address:port\' at which baresip listens
|
||||
for incoming SIP requests. If IP address is an IPv6 address, it must be written inside
|
||||
brackets []. IPv4 address 0.0.0.0 or IPv6 address [::] makes baresip listen at all
|
||||
available addresses. If left empty (factory default), baresip listens at port 5060 of
|
||||
all available addresses.
|
||||
</string>
|
||||
<string name="dns_servers_help">Comma separated list of addresses of DNS servers. If not given,
|
||||
DNS server addresses are obtained dynamically from the system. Each DNS address is of form
|
||||
\'ip:port\' or \'ip\'. If port is omitted, it defaults to 53. If ip is an IPv6 address and
|
||||
@ -290,8 +295,6 @@
|
||||
<string name="tls_ca_file">TLS CA証明書ファイル</string>
|
||||
<string name="audio_modules_title">オーディオモジュール</string>
|
||||
<string name="failed_to_load_module">モジュールの読み込みに失敗しました</string>
|
||||
<string name="aec_help">チェックを入れると、通話音声のエコーキャンセルを試みます</string>
|
||||
<string name="aec_extended_filter">音響エコーキャンセル拡張フィルタ</string>
|
||||
<string name="opus_bit_rate">Opusのビットレート</string>
|
||||
<string name="opus_packet_loss">期待されるOpusのパケットロス</string>
|
||||
<string name="invalid_opus_bitrate">Opusのビットレートが無効です</string>
|
||||
@ -346,7 +349,6 @@
|
||||
<string name="answer_mode_help">着信した電話にどのように応答するかを選択します。</string>
|
||||
<string name="start_automatically_help">チェックを入れると、デバイスの再起動後に自動的にbaresipが起動します</string>
|
||||
<string name="aec">音響エコーキャンセル</string>
|
||||
<string name="aec_extended_filter_help">チェックを入れると、エコーキャンセルは拡張フィルタを使用しています。</string>
|
||||
<string name="default_call_volume_help">設定されている場合、デフォルトの通話音声の音量は1~10段階です</string>
|
||||
<string name="dark_theme_help">ダークテーマを強制する</string>
|
||||
<string name="debug_help">チェックを入れると、デバッグおよび情報レベルのログメッセージをLogcatに提供します。</string>
|
||||
@ -363,4 +365,4 @@
|
||||
<string name="transfer_request_query"> %1$s への通話転送を許可しますか?</string>
|
||||
<string name="backed_up">アプリケーションデータがダウンロードフォルダ %1$s にバックアップされました</string>
|
||||
<string name="no_calls">マイクへの権限付与がなく電話をかけたり、応答したりすることはできません。</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@ -130,7 +130,6 @@
|
||||
<string name="dns_servers_help">Kommainndelt liste over adresser til DNS-tjenere. Hvis ikke angitt, vil DNS-tjeneradresser hentes dynamisk fra systemet. Hver DNS-adresse er utførelsen \"ip:port\" eller \"ip\". Hvis porten utelates, brukes forvalget 53. Hvis IP-en er en IPv6-adresse, og port også angis, må IP-en skrives i hakeparenteser []. Som etksempel, list \'8.8.8.8:53,[2001:4860:4860::8888]:53\' peker til en IPv4-adresser og IPv6-adresser for offentlige Google DNS-tjenere.</string>
|
||||
<string name="failed_to_load_module">Klarte ikke å laste inn modul.</string>
|
||||
<string name="aec">Kansellering av akustisk ekko</string>
|
||||
<string name="aec_help">Prøver å unngå ekko under samtaler.</string>
|
||||
<string name="opus_bit_rate_help">Gjennomsnittlig maksimal bitrate ved bruk av Opus-lydstrøm. Gyldig verdi er 6000-510000. Fabrikksforvalg er 28000.</string>
|
||||
<string name="opus_packet_loss">Forventet Opus-pakketap</string>
|
||||
<string name="opus_packet_loss_help">Forventet prosentvis pakketap for Opus-lydstrøm. Gyldige verdier fra 0–100. Fabrikksforvalg er 0, som skrur av Opus-fremtidsfeilkorrigering (FEC).</string>
|
||||
@ -231,4 +230,4 @@
|
||||
<string name="time">Tid</string>
|
||||
<string name="calls_duration">Varighet</string>
|
||||
<string name="audio_settings">Lydinnstillinger</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@ -91,7 +91,6 @@
|
||||
<string name="opus_packet_loss">Perda de pacotes Opus esperada</string>
|
||||
<string name="opus_bit_rate_help">Taxa de bits máxima média usada pelo fluxo de áudio do Opus. Os valores válidos são 6000-510000. O padrão de fábrica é 28000.</string>
|
||||
<string name="opus_bit_rate">Taxa de Bits Opus</string>
|
||||
<string name="aec_help">Se estiver marcado, o cancelamento de eco baseado em software está em uso.</string>
|
||||
<string name="aec">Cancelamento de Eco Acústico</string>
|
||||
<string name="failed_to_load_module">Falha ao carregar módulo.</string>
|
||||
<string name="audio_modules_help">Codecs de áudio providos pelos módulos selecionados estão disponíveis para uso das contas.</string>
|
||||
@ -249,8 +248,6 @@
|
||||
<string name="confirmation">Confirmação</string>
|
||||
<string name="video_size_help">Tamanho dos quadros de vídeo que foram transmitidos (largura x altura)</string>
|
||||
<string name="video_size">Tamanho do Quadro do Vídeo</string>
|
||||
<string name="aec_extended_filter_help">Caso esteja marcado, o cancelamento do eco estará utilizando um filtro estendido.</string>
|
||||
<string name="aec_extended_filter">Filtro Estendido do AEC</string>
|
||||
<string name="invalid_stun_password">Senha inválida \'%1$s\'</string>
|
||||
<string name="stun_password_help">Senha, caso seja necessário pelo servidor STUN/TURN</string>
|
||||
<string name="stun_password">Senha STUN/TURN</string>
|
||||
@ -425,5 +422,4 @@
|
||||
<string name="invalid_microphone_gain">Valor inválido do ganho do microfone</string>
|
||||
<string name="microphone_gain">Ganho do microfone</string>
|
||||
<string name="microphone_gain_help">Multiplique o volume do microfone por esse número decimal. O valor mínimo é 1,0 (padrão de fábrica), que desativa o ganho do microfone. Valores maiores podem afetar negativamente a qualidade do áudio.</string>
|
||||
<string name="no_hw_aec">O cancelador de eco baseado em hardware não pode ser usado em seu dispositivo.</string>
|
||||
</resources>
|
||||
|
||||
@ -43,7 +43,6 @@
|
||||
<string name="chat">Mensagem do Chat</string>
|
||||
<string name="contact_action_question">Quer ligar ou enviar mensagem para \'%1$s\'\?</string>
|
||||
<string name="tls_ca_file">Ficheiro TLS CA</string>
|
||||
<string name="aec_extended_filter_help">Caso esteja marcado, o cancelamento do eco estará utilizando um filtro estendido.</string>
|
||||
<string name="sip_uri_of_another_proxy_server">URI SIP de outro Servidor Proxy</string>
|
||||
<string name="contact_already_exists">Contato \'%1$s\' já existe.</string>
|
||||
<string name="sip_uri_of_proxy_server">URI SIP do Servidor Proxy</string>
|
||||
@ -122,14 +121,12 @@
|
||||
<string name="no_messages">Não tem mensagens</string>
|
||||
<string name="duration">Duração: %1$d (segs)</string>
|
||||
<string name="transferring_call_to_dots">Transferindo chamada para …</string>
|
||||
<string name="aec_help">Se marcado, cancelamento de eco é tentado no áudio da chamada.</string>
|
||||
<string name="call_info">Informações da Chamada</string>
|
||||
<string name="chats">Histórico de Mensagens</string>
|
||||
<string name="call_is_secure">A chamada é SEGURA e o par é VERIFICADO! Deseja desverificar o par\?</string>
|
||||
<string name="opus_bit_rate_help">Taxa de bits máxima média usada pelo fluxo de áudio do Opus. Os valores válidos são 6000-510000. A predefinição de fábrica é 28000.</string>
|
||||
<string name="restart_request">Solicitação para Reinicialização</string>
|
||||
<string name="register">Registo</string>
|
||||
<string name="aec_extended_filter">Filtro Estendido do AEC</string>
|
||||
<string name="restore">Restaurar</string>
|
||||
<string name="opus_packet_loss">Perda de pacotes Opus esperada</string>
|
||||
<string name="transfer_request_query">Aceita transferir esta chamada para \'%1$s\'\?</string>
|
||||
@ -325,4 +322,4 @@
|
||||
<string name="tone_country_help">Toque da chamada do país, espera e tom de chamada ocupada</string>
|
||||
<string name="rel_100">Respostas provisórias confiáveis</string>
|
||||
<string name="rel_100_help">Se for marcado, indica suporte para respostas provisórias confiáveis (RFC 3262).</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@ -62,7 +62,6 @@
|
||||
<string name="listen_address">Adresă de conectare</string>
|
||||
<string name="dns_servers">Servere DNS</string>
|
||||
<string name="aec">Anularea acustică a ecoului</string>
|
||||
<string name="aec_help">Dacă este bifat, se încearcă eliminarea ecoului în timpul apelurilor.</string>
|
||||
<string name="opus_bit_rate">Rată de eșantionare Opus</string>
|
||||
<string name="default_call_volume">Volum implicit de apel</string>
|
||||
<string name="default_call_volume_help">Dacă este setat, volumul audio de apel implicit între 1–10.</string>
|
||||
|
||||
@ -134,7 +134,6 @@
|
||||
<string name="account_exists">Профиль \'%1$s\' уже существует.</string>
|
||||
<string name="accounts">Профили</string>
|
||||
<string name="aec">Подавление акустического эха</string>
|
||||
<string name="aec_help">Если выбрано - работает подавление эха.</string>
|
||||
<string name="and">и</string>
|
||||
<string name="answer_mode">Режим ответа</string>
|
||||
<string name="answer_mode_help">Выберите как отвечать на входящие вызовы.</string>
|
||||
@ -181,8 +180,6 @@
|
||||
<string name="opus_packet_loss">Ожидаемая потеря пакетов Opus</string>
|
||||
<string name="opus_bit_rate_help">Средняя максимальная скорость передачи данных, используемая в аудиопотоке Opus. Допустимые значения: 6000-510000. Значение по умолчанию - 28000.</string>
|
||||
<string name="opus_bit_rate">Скорость передачи данных Opus</string>
|
||||
<string name="aec_extended_filter_help">Если этот флажок установлен, для подавления эха используется расширенный фильтр.</string>
|
||||
<string name="aec_extended_filter">Расширенный фильтр AEC</string>
|
||||
<string name="audio_modules_help">Аудиокодеки, предоставленные выбранными модулями, доступны для использования в аккаунтах.</string>
|
||||
<string name="tls_ca_file_help">При отметке файл с TLS-сертификатами необходимых центров сертификации был или будет загружен. В Android версии 9 и ниже файл называется \'ca_certs.crt\' и берётся из каталога загрузок.</string>
|
||||
<string name="tls_ca_file">Файл TLS CA</string>
|
||||
@ -328,4 +325,4 @@
|
||||
<string name="rel_100">Надёжные предварительные ответы</string>
|
||||
<string name="appear_on_top_permission">Для автоматического запуска требуется разрешение отображения поверх других приложений.</string>
|
||||
<string name="video_fps">Кадровая частота</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@ -219,12 +219,9 @@
|
||||
<string name="transfer_request">Kopplingsbegäran</string>
|
||||
<string name="default_call_volume_help">Kryssa i för att använda en förvald ljudvolym, 1–10, vid samtal.</string>
|
||||
<string name="accept">Acceptera</string>
|
||||
<string name="aec_extended_filter">Utökat filter vid ekoutsläckning</string>
|
||||
<string name="android_contact_help">Kryssa i för att lägga till kontakten till dina Android-kontakter.</string>
|
||||
<string name="sip_trace_help">Kryssa i för att inkludera SIP-meddelanden i Logcat-meddelanden när Felsökning är aktiverat. Funktionen inaktiveras automatiskt när baresip startar.</string>
|
||||
<string name="debug_help">Kryssa i för att skicka felsöknings- och info-meddelanden till Logcat.</string>
|
||||
<string name="aec_extended_filter_help">Kryssa i för att använda ett utökat filter vid ekoutsläckning.</string>
|
||||
<string name="aec_help">Kryssa i för att aktivera ekoutsläckning för samtalsljud.</string>
|
||||
<string name="tls_ca_file_help">Kryssa i för att ladda TLS-certifikat för certifikatauktoriteter från fil. I Android 9 och tidigare versioner laddas en fil med namnet \'ca_certs.crt\' från katalogen Nedladdningar.</string>
|
||||
<string name="verify_server_help">Kryssa i om baresip ska verifiera TLS-certifikat för SIP user agent och SIP proxyserver när transportprotokollet TLS används. TLS CA-fil måste då också laddas.</string>
|
||||
<string name="tls_certificate_file_help">Kryssa i för att ladda TLS-certifikat och privat nyckel för den här baresip-instansen från fil. I Android 9 och tidigare versioner laddas en fil med namnet \'cert.pem\' från katalogen Nedladdningar. Av säkerhetsskäl tas filen bort efter att den lästs in.</string>
|
||||
@ -312,4 +309,4 @@
|
||||
<string name="reg_int_help">Avgör hur ofta (i sekunder) baresip skickar REGISTER-förfrågningar. Giltiga värden är från 60 till 3600.</string>
|
||||
<string name="audio_delay_help">Tid (i millisekunder) att vänta på ljud från motparten när ett samtal upprättats. Sätt till ett högre värde om du saknar ljud från motparten i början av samtalet.</string>
|
||||
<string name="audio_and_video_permissions">baresip+ behöver behörigheten \"Mikrofon\" för röstsamtal, behörigheten \"Kamera\" för videosamtal, behörigheten \"Enheter i närheten\" för upptäckt av mikrofon/högtalare via blåtand och behörigheten \"Aviseringar\" för att skicka aviseringar.</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@ -171,8 +171,6 @@
|
||||
<string name="audio_settings">ஆடியோ அமைப்புகள்</string>
|
||||
<string name="tls_ca_file_help">சரிபார்க்கப்பட்டால், ஆண்ட்ராய்டு OS இல் சேர்க்கப்படாத அத்தகைய சான்றிதழ் அதிகாரிகளின் TLS சான்றிதழ்களைக் கொண்ட ஒரு கோப்பு அல்லது ஏற்றப்படும். ஆண்ட்ராய்டு பதிப்பு 9 இல், \'Ca_certs.crt\' என்ற கோப்பு பதிவிறக்க கோப்புறையிலிருந்து ஏற்றப்படுகிறது.</string>
|
||||
<string name="speaker_phone">அவைத்தலைவர் தொலைபேசி</string>
|
||||
<string name="aec_extended_filter">AEC நீட்டிக்கப்பட்ட வடிகட்டி</string>
|
||||
<string name="aec_extended_filter_help">சரிபார்க்கப்பட்டால், எதிரொலி ரத்துசெய்தல் நீட்டிக்கப்பட்ட வடிப்பானைப் பயன்படுத்துகிறது.</string>
|
||||
<string name="microphone_gain_help">இந்த தசம எண்ணால் மைக்ரோஃபோன் அளவைப் பெருக்கவும். மைக்ரோஃபோன் ஆதாயத்தை முடக்கும் குறைந்தபட்ச மதிப்பு 1.0 (தொழிற்சாலை இயல்புநிலை) ஆகும். பெரிய மதிப்புகள் ஆடியோ தரத்தை எதிர்மறையாக பாதிக்கலாம்.</string>
|
||||
<string name="speaker_phone_help">சரிபார்க்கப்பட்டால், அழைப்பு தொடங்கும் போது ச்பீக்கர் தொலைபேசி தானாக இயக்கப்படும்.</string>
|
||||
<string name="audio_modules_title">ஆடியோ தொகுதிகள்</string>
|
||||
|
||||
@ -8,8 +8,10 @@
|
||||
<string name="about_text">
|
||||
<![CDATA[
|
||||
<h1>Baresip library based SIP User Agent</h1>
|
||||
<br>
|
||||
<p>Juha Heinanen <jh@tutpro.com></p>
|
||||
<p>Version %1$s</p>
|
||||
<br>
|
||||
<h2>Usage Hints</h2>
|
||||
<ul>
|
||||
<li>Check that default values in baresip\'s Settings meet your needs
|
||||
@ -21,19 +23,19 @@
|
||||
<li>Touch on the dot leads directly to account configuration.</li>
|
||||
<li>Swipe down gesture causes re-registration of the currently shown account.</li>
|
||||
<li>Long touch on currently shown account enables or disables account\'s registration.</li>
|
||||
<li>Swipe left/right gesture toggles between the accounts.</li>
|
||||
<li>Previous call party can be reselect by touching the call icon when Callee is empty.</li>
|
||||
<li>Peers of calls and messages can be added to contacts by long touches.</li>
|
||||
<li>Long touches can also be used to remove calls, chats, messages, and contacts.</li>
|
||||
<li>Touch/long touch of contact icon can be used to install/remove image avatar.</li>
|
||||
<li>See <a href="https://github.com/juha-h/baresip-studio/wiki">Wiki</a> for more
|
||||
information.</li>
|
||||
<li>See <a href="https://github.com/juha-h/baresip-studio/wiki">Wiki</a> for more information.</li>
|
||||
</ul>
|
||||
<h2>Privacy Policy</h2>
|
||||
Privacy policy is available <a href="https://raw.githubusercontent.com/juha-h/baresip-studio/master/PrivacyPolicy.txt">here</a>.
|
||||
<p>Privacy policy is available <a href="https://raw.githubusercontent.com/juha-h/baresip-studio/master/PrivacyPolicy.txt">here</a>.</p>
|
||||
<br>
|
||||
<h2>Source code</h2>
|
||||
Source code is available at <a href="https://github.com/juha-h/baresip-studio">GitHub</a>,
|
||||
where also issues can be reported.
|
||||
<p>Source code is available at <a href="https://github.com/juha-h/baresip-studio">GitHub</a>,
|
||||
where also issues can be reported.</p>
|
||||
<br>
|
||||
<h2>Licenses</h2>
|
||||
<ul>
|
||||
<li><b>BSD-3-Clause</b> except the following:</li>
|
||||
@ -210,10 +212,10 @@
|
||||
</string>
|
||||
<!-- Accounts Activity -->
|
||||
<string name="accounts">Accounts</string>
|
||||
<string name="new_account">New Account</string>
|
||||
<string name="accounts_help">When a new account is created, account\'s port number and transport
|
||||
protocol may be optionally given: <user>@<domain>[:<port>][;transport=udp|tcp|tls].
|
||||
If <port> is given and transport protocol is not given, transport protocol defaults to udp.
|
||||
<string name="new_account">SIP URI of New Account</string>
|
||||
<string name="accounts_help">SIP URI of new account of form
|
||||
<user>@<domain>[:<port>][;transport=udp|tcp|tls].
|
||||
If <port> is given and transport protocol is not given, transport protocol defaults to UDP.
|
||||
If <port> is not given and transport protocol is given, <port> defaults to 5060 or 5061 (TLS).
|
||||
If neither is given and no outbound proxy is specified, account\'s registrar (if any) is
|
||||
determined solely based on domain\'s DNS information.
|
||||
@ -278,10 +280,10 @@
|
||||
<!-- Codecs Activity -->
|
||||
<string name="audio_codecs">Audio Codecs</string>
|
||||
<string name="audio_codecs_help">List of audio codecs in priority order. Drag to reorder,
|
||||
swipe right to enable or disable.</string>
|
||||
long click to enable or disable.</string>
|
||||
<string name="video_codecs">Video Codecs</string>
|
||||
<string name="video_codecs_help">List of video codecs in priority order. Drag to reorder,
|
||||
swipe right to enable or disable.</string>
|
||||
long click to enable or disable.</string>
|
||||
<string name="codec_action">Reorder</string>
|
||||
<!-- Config Activity -->
|
||||
<string name="configuration">Settings</string>
|
||||
@ -340,10 +342,6 @@
|
||||
available for use by the accounts.</string>
|
||||
<string name="failed_to_load_module">Failed to load module.</string>
|
||||
<string name="aec">Acoustic Echo Cancellation</string>
|
||||
<string name="aec_help">If checked, software based echo cancellation is in use.</string>
|
||||
<string name="no_hw_aec">Hardware based echo canceller cannot be used on you device.</string>
|
||||
<string name="aec_extended_filter">AEC Extended Filter</string>
|
||||
<string name="aec_extended_filter_help">If checked, echo cancellation is using extended filter.</string>
|
||||
<string name="microphone_gain">Microphone Gain</string>
|
||||
<string name="microphone_gain_help">Multiply microphone volume by this decimal number. Minimum
|
||||
value is 1.0 (factory default) that disables microphone gain. Larger values may negatively
|
||||
@ -413,6 +411,8 @@
|
||||
<string name="sip_or_tel_uri">SIP or tel URI</string>
|
||||
<string name="user_domain_or_number">user@domain or telephone number</string>
|
||||
<string name="favorite">Favorite</string>
|
||||
<string name="favorite_help">If checked, contact is shown among other favorites
|
||||
on top of contacts list.</string>
|
||||
<string name="invalid_contact">Invalid contact name \'%1$s\'</string>
|
||||
<string name="contact_already_exists">Contact \'%1$s\' already exists.</string>
|
||||
<string name="invalid_contact_uri">Invalid SIP URI</string>
|
||||
@ -452,6 +452,8 @@
|
||||
<string name="invalid_sip_or_tel_uri">Invalid SIP or tel URI \'%1$s\'</string>
|
||||
<string name="baresip" translatable="false">baresip</string>
|
||||
<string name="android" translatable="false">Android</string>
|
||||
<string name="hardware">Hardware</string>
|
||||
<string name="software">Software</string>
|
||||
<!-- Main Activity -->
|
||||
<string name="backup">Backup</string>
|
||||
<string name="restore">Restore</string>
|
||||
|
||||
@ -1,50 +1,23 @@
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="actionBarTheme">@style/ActionBar</item>
|
||||
<item name="actionOverflowMenuStyle">@style/Spinner</item>
|
||||
<item name="android:dropDownListViewStyle">@style/Spinner</item>
|
||||
<item name="android:colorBackground">@color/colorBackground</item>
|
||||
<item name="android:windowBackground">@color/colorBackground</item>
|
||||
<item name="android:statusBarColor">@color/colorBackground</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Main" parent="AppTheme">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
<style name="ActionBar" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar">
|
||||
<item name="android:colorBackground">@color/colorSecondaryDark</item>
|
||||
<item name="colorSurface">@color/colorPrimary</item>
|
||||
<item name="android:textColor">@color/colorLight</item>
|
||||
<item name="android:textColorPrimary">@color/colorLight</item>
|
||||
<item name="android:textColorSecondary">@color/colorLight</item>
|
||||
</style>
|
||||
|
||||
<style name="Spinner" parent="Widget.AppCompat.ListView.DropDown">
|
||||
<item name="android:popupBackground">@color/colorPopupBackground</item>
|
||||
<item name="android:textColor">@color/colorLight</item>
|
||||
<item name="android:dividerHeight">1dp</item>
|
||||
<item name="android:divider">@color/colorSpinnerDivider</item>
|
||||
</style>
|
||||
|
||||
<style name="AlertDialogTheme">
|
||||
<style name="AlertDialogTheme" parent="@style/ThemeOverlay.MaterialComponents.MaterialAlertDialog">
|
||||
<item name="materialAlertDialogTitleTextStyle">@style/Alert.Title</item>
|
||||
<item name="materialAlertDialogBodyTextStyle">@style/Alert.Message</item>
|
||||
<item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item>
|
||||
<item name="buttonBarNegativeButtonStyle">@style/Alert.Button.Positive</item>
|
||||
<item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item>
|
||||
<item name="materialAlertDialogBodyTextStyle">@style/Alert.Message</item>
|
||||
<item name="materialAlertDialogTitleTextStyle">@style/Alert.Title</item>
|
||||
<item name="shapeAppearanceOverlay">@style/DialogCorners</item>
|
||||
<item name="android:background">@color/colorGrayLight</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
<style name="DialogCorners">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">16dp</item>
|
||||
<style name="Alert.Title" parent="@style/MaterialAlertDialog.Material3.Title.Text">
|
||||
<item name="android:textColor">@color/colorAlert</item>
|
||||
<item name="android:textSize">20sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Alert.Message" parent="@style/MaterialAlertDialog.Material3.Body.Text">
|
||||
<item name="android:textColor">@color/colorDark</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Alert.Button.Positive" parent="Widget.Material3.Button.TextButton">
|
||||
@ -61,14 +34,4 @@
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Alert.Title" parent="@style/MaterialAlertDialog.Material3.Title.Text">
|
||||
<item name="android:textColor">@color/colorAlert</item>
|
||||
<item name="android:textSize">20sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Alert.Message" parent="@style/MaterialAlertDialog.Material3.Body.Text">
|
||||
<item name="android:textColor">@color/colorStrong</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@ -8,7 +8,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
// noinspection AndroidGradlePluginVersion
|
||||
classpath 'com.android.tools.build:gradle:8.8.1'
|
||||
classpath libs.gradle.v881
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
68
gradle/libs.versions.toml
Normal file
68
gradle/libs.versions.toml
Normal file
@ -0,0 +1,68 @@
|
||||
[versions]
|
||||
activityCompose = "1.10.1"
|
||||
appcompat = "1.7.0"
|
||||
cardview = "1.0.0"
|
||||
coilCompose = "2.5.0"
|
||||
composeBom = "2025.03.01"
|
||||
constraintlayout = "2.2.1"
|
||||
coreKtx = "1.15.0"
|
||||
exifinterface = "1.4.0"
|
||||
foundation = "1.7.8"
|
||||
fragmentKtx = "1.8.6"
|
||||
gradle = "8.9.1"
|
||||
gradleVersion = "8.9.1"
|
||||
kotlin = "2.0.21"
|
||||
kotlinStdlibJdk8 = "2.1.0"
|
||||
kotlinxCoroutinesAndroid = "1.9.0"
|
||||
lifecycleExtensions = "2.2.0"
|
||||
lifecycleViewmodelCompose = "2.8.7"
|
||||
localbroadcastmanager = "1.1.0"
|
||||
material = "1.12.0"
|
||||
media = "1.7.0"
|
||||
preferenceKtx = "1.2.1"
|
||||
swiperefreshlayout = "1.1.0"
|
||||
ui = "1.8.0-rc02"
|
||||
foundationAndroid = "1.7.8"
|
||||
toolsCore = "1.0.0-alpha13"
|
||||
runtimeLivedata = "1.7.8"
|
||||
uiTestAndroid = "1.7.8"
|
||||
composeMaterial = "1.7.8"
|
||||
composeMaterial3 = "1.3.1"
|
||||
|
||||
[libraries]
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
||||
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityCompose" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
|
||||
androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "cardview" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
||||
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
|
||||
androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterface" }
|
||||
androidx-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
|
||||
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
|
||||
androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycleExtensions" }
|
||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
|
||||
androidx-localbroadcastmanager = { module = "androidx.localbroadcastmanager:localbroadcastmanager", version.ref = "localbroadcastmanager" }
|
||||
androidx-media = { module = "androidx.media:media", version.ref = "media" }
|
||||
androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
|
||||
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
|
||||
androidx-ui = { module = "androidx.compose.ui:ui", version.ref = "ui" }
|
||||
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
|
||||
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
|
||||
gradle-v881 = { module = "com.android.tools.build:gradle", version.ref = "gradleVersion" }
|
||||
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlinStdlibJdk8" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||
ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-foundation-android = { group = "androidx.compose.foundation", name = "foundation-android", version.ref = "foundationAndroid" }
|
||||
androidx-tools-core = { group = "androidx.privacysandbox.tools", name = "tools-core", version.ref = "toolsCore" }
|
||||
androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
|
||||
androidx-ui-test-android = { group = "androidx.compose.ui", name = "ui-test-android", version.ref = "uiTestAndroid" }
|
||||
androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "composeMaterial" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "composeMaterial3" }
|
||||
|
||||
[plugins]
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Mon Jan 30 05:45:34 EET 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user