Browse Source

sip status handling with enums

mikhail 6 months ago
parent
commit
2feb53437b

+ 7 - 4
app/src/main/java/ru/mephi/voip/call/abto/AccountStatus.kt

@@ -1,7 +1,10 @@
 package ru.mephi.voip.call.abto
 
-enum class AccountStatus {
-    NO_CONNECTION,
-    UNREGISTERED,
-    REGISTERED,
+enum class AccountStatus(val status: String) {
+    NO_CONNECTION("Сеть недоступна"),
+    LOADING("Подключение..."),
+    UNREGISTERED("Не зарегистрирован"),
+    REGISTRATION_FAILED("Ошибка регистрации"),
+    REGISTERED("Аккаунт активен"),
+    CHANGING("Смена аккаунта..."),
 }

+ 33 - 60
app/src/main/java/ru/mephi/voip/ui/MainActivity.kt

@@ -1,14 +1,9 @@
 package ru.mephi.voip.ui
 
 import android.Manifest
-import android.annotation.TargetApi
-import android.app.AlertDialog
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
+import android.app.*
 import android.content.Intent
 import android.content.SharedPreferences
-import android.content.pm.PackageManager
 import android.os.Build
 import android.os.Bundle
 import androidx.core.app.NotificationCompat
@@ -17,9 +12,9 @@ import androidx.navigation.NavController
 import androidx.preference.PreferenceManager
 import com.google.firebase.analytics.FirebaseAnalytics
 import com.google.firebase.analytics.ktx.analytics
-import com.google.firebase.crashlytics.FirebaseCrashlytics
 import com.google.firebase.ktx.Firebase
 import com.vmadalin.easypermissions.EasyPermissions
+import com.vmadalin.easypermissions.dialogs.SettingsDialog
 import org.abtollc.sdk.*
 import org.abtollc.sdk.OnInitializeListener.InitializeState
 import org.abtollc.utils.codec.Codec
@@ -29,7 +24,8 @@ import ru.mephi.voip.data.utils.*
 import ru.mephi.voip.databinding.ActivityMainBinding
 import timber.log.Timber
 
-class MainActivity : NetworkSensingBaseActivity(), OnInitializeListener, OnRegistrationListener {
+class MainActivity : NetworkSensingBaseActivity(), OnInitializeListener, OnRegistrationListener,
+    EasyPermissions.PermissionCallbacks {
     private val CHANNEL_ID: String = "CHANNEL"
     private lateinit var binding: ActivityMainBinding
     private val REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124
@@ -67,6 +63,7 @@ class MainActivity : NetworkSensingBaseActivity(), OnInitializeListener, OnRegis
         binding = ActivityMainBinding.inflate(layoutInflater)
         setContentView(binding.root)
 
+
         firebaseAnalytics = Firebase.analytics
 
         if (!hasPermissions())
@@ -120,69 +117,23 @@ class MainActivity : NetworkSensingBaseActivity(), OnInitializeListener, OnRegis
         currentNavController = navController
     }
 
-    @TargetApi(23)
-    override fun onRequestPermissionsResult(
-        requestCode: Int,
-        permissions: Array<String>,
-        grantResults: IntArray
-    ) {
-        when (requestCode) {
-            REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS -> {
-                val perms: MutableMap<String, Int> = HashMap()
-                //Initial
-                perms[Manifest.permission.RECORD_AUDIO] = PackageManager.PERMISSION_GRANTED
-                perms[Manifest.permission.WRITE_EXTERNAL_STORAGE] =
-                    PackageManager.PERMISSION_GRANTED
-                perms[Manifest.permission.USE_SIP] = PackageManager.PERMISSION_GRANTED
-
-                //Fill with results
-                var i = 0
-                while (i < permissions.size) {
-                    perms[permissions[i]] = grantResults[i]
-                    i++
-                }
-
-                //Check for ACCESS_FINE_LOCATION
-                if (perms[Manifest.permission.RECORD_AUDIO] == PackageManager.PERMISSION_GRANTED
-                    && perms[Manifest.permission.WRITE_EXTERNAL_STORAGE] == PackageManager.PERMISSION_GRANTED
-                    && perms[Manifest.permission.USE_SIP] == PackageManager.PERMISSION_GRANTED
-                ) {
-                    // All Permissions Granted
-                    initPhone()
-                } else {
-                    // Permission Denied
-                    toast("Some permissions were denied")
-                    finish()
-                }
-            }
-            else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
-        }
-    }
-
-    private fun hasPermissions(): Boolean =
+    fun hasPermissions(): Boolean =
         EasyPermissions.hasPermissions(
             this,
             Manifest.permission.RECORD_AUDIO,
             Manifest.permission.USE_SIP
         )
 
-    private fun requestPermissions() {
+    fun requestPermissions() {
         EasyPermissions.requestPermissions(
             this,
             "Это приложение требует разрешение на совершение звонков и использование микрофона",
-            1,
+            REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS,
             Manifest.permission.RECORD_AUDIO,
             Manifest.permission.USE_SIP
         )
     }
 
-    // После отклонения входящего вызова из шторки потом не делает исходящий
-    override fun onRestart() {
-        super.onRestart()
-//        initPhone()
-//        initAccount()
-    }
-
     override fun onSupportNavigateUp(): Boolean {
         return currentNavController?.value?.navigateUp() ?: false
     }
@@ -191,9 +142,6 @@ class MainActivity : NetworkSensingBaseActivity(), OnInitializeListener, OnRegis
         if (abtoPhone.isActive)
             return
 
-//        val accId = abtoPhone.currentAccountId.toInt()
-//        accExpire = abtoPhone.config.getAccountExpire(accId.toLong())
-
         val domain = SIP_DOMAIN
         val account = getActiveAccount()
         val username = account?.login
@@ -293,4 +241,29 @@ class MainActivity : NetworkSensingBaseActivity(), OnInitializeListener, OnRegis
     override fun onRegistrationFailed(accId: Long, statusCode: Int, statusText: String?) {
         toast("Аккаунт \"${abtoPhone.getSipUsername(accId) ?: "null"}\" не зарегистрирован. Причина: $statusText")
     }
+
+    override fun onPermissionsDenied(requestCode: Int, perms: List<String>) {
+        if (EasyPermissions.somePermissionPermanentlyDenied(this@MainActivity, perms)) {
+            SettingsDialog.Builder(this)
+                .title("Запрос разрешений для SIP-звонков")
+                .rationale("Это приложение требует разрешения на совершение звонков и использование микрофона")
+                .negativeButtonText("Закрыть")
+                .positiveButtonText("Предоставить")
+                .build().show()
+        } else
+            requestPermissions()
+    }
+
+    override fun onPermissionsGranted(requestCode: Int, perms: List<String>) {
+
+    }
+
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<out String>,
+        grantResults: IntArray
+    ) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
+    }
 }

+ 11 - 7
app/src/main/java/ru/mephi/voip/ui/calls/CallerFragment.kt

@@ -34,6 +34,7 @@ import ru.mephi.voip.data.utils.slideDown
 import ru.mephi.voip.data.utils.slideUp
 import ru.mephi.voip.data.utils.toast
 import ru.mephi.voip.databinding.FragmentCallsBinding
+import ru.mephi.voip.ui.MainActivity
 import ru.mephi.voip.ui.calls.adapter.CallHistoryAdapter
 import ru.mephi.voip.ui.calls.adapter.SwipeToDeleteCallback
 import timber.log.Timber
@@ -90,10 +91,11 @@ class CallerFragment : Fragment() {
             )
     }
 
-    private fun tryToCall() {
-        val callNumber = binding.numPad.getInputNumber()
-        CallActivity.create(requireContext(), callNumber, false)
-    }
+    private fun tryToCall() =
+        if ((activity as MainActivity).hasPermissions())
+            CallActivity.create(requireContext(), binding.numPad.getInputNumber(), false)
+        else
+            (activity as MainActivity).requestPermissions()
 
     private fun initViews() {
         val args: CallerFragmentArgs by navArgs()
@@ -105,7 +107,8 @@ class CallerFragment : Fragment() {
             else
                 requireActivity().finish()
         }
-        binding.cardView.visibility = View.INVISIBLE
+
+        binding.cardView.visibility = View.GONE
 
         binding.fabOpenNumpad.setOnClickListener {
             changeNumPadVisibility()
@@ -126,7 +129,8 @@ class CallerFragment : Fragment() {
         }
 
         if (isPermissionGranted)
-            binding.fabOpenNumpad.backgroundTintList = ColorStateList.valueOf(Color.rgb(76, 175, 80))
+            binding.fabOpenNumpad.backgroundTintList =
+                ColorStateList.valueOf(Color.rgb(76, 175, 80))
         else
             binding.fabOpenNumpad.backgroundTintList = ColorStateList.valueOf(Color.GRAY)
 
@@ -169,7 +173,7 @@ class CallerFragment : Fragment() {
         val screenW = getScreenWidth()
         val margin = binding.fabOpenNumpad.marginRight
         val fabWidth = binding.fabOpenNumpad.width
-        val mid = (screenW / 2 - margin).toFloat() - fabWidth/5.5f
+        val mid = (screenW / 2 - margin).toFloat() - fabWidth / 5.5f
 
         if (isNumPadUp) {
             binding.cardView.slideDown()

+ 10 - 1
app/src/main/java/ru/mephi/voip/ui/calls/adapter/CallHistoryAdapter.kt

@@ -8,10 +8,12 @@ import android.view.ViewGroup
 import androidx.annotation.RequiresApi
 import androidx.recyclerview.widget.RecyclerView
 import ru.mephi.voip.R
+import ru.mephi.voip.appContext
 import ru.mephi.voip.call.ui.CallActivity
 import ru.mephi.voip.data.database.calls.CallRecord
 import ru.mephi.voip.data.database.calls.LocalDateTimeConverter
 import ru.mephi.voip.databinding.ItemCallRecordBinding
+import ru.mephi.voip.ui.MainActivity
 import ru.mephi.voip.ui.catalog.adapter.BaseViewHolder
 
 
@@ -78,7 +80,14 @@ class CallHistoryAdapter internal constructor(var context: Context) :
                 }
 
                 call.setOnClickListener {
-                    CallActivity.create(this.root.context, binding.sipNumber.text.toString(), false)
+                    if ((context as MainActivity).hasPermissions())
+                        CallActivity.create(
+                            this.root.context,
+                            binding.sipNumber.text.toString(),
+                            false
+                        )
+                    else
+                        (context as MainActivity).requestPermissions()
                 }
 
                 sipNumber.text = item.sipNumber

+ 40 - 20
app/src/main/java/ru/mephi/voip/ui/profile/ProfileFragment.kt

@@ -1,6 +1,5 @@
 package ru.mephi.voip.ui.profile
 
-import android.graphics.Color.rgb
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -20,6 +19,7 @@ import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.CheckCircle
 import androidx.compose.material.icons.filled.Delete
 import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.materialIcon
 import androidx.compose.runtime.*
 import androidx.compose.runtime.livedata.observeAsState
 import androidx.compose.ui.Alignment
@@ -60,6 +60,7 @@ import ru.mephi.voip.R
 import ru.mephi.voip.call.abto.AccountStatus
 import ru.mephi.voip.data.model.Account
 import ru.mephi.voip.data.model.NameItem
+import ru.mephi.voip.ui.MainActivity
 
 @ExperimentalComposeUiApi
 @ExperimentalCoilApi
@@ -298,14 +299,17 @@ class ProfileFragment : Fragment() {
             Box {
                 IconButton(
                     onClick = {
-                        viewModel.updateActiveAccount(account)
+                        if ((context as MainActivity).hasPermissions()) {
+                            viewModel.updateActiveAccount(account)
+//                            Toast.makeText(
+//                                context,
+//                                "Активный аккаунт: ${viewModel.newLogin.value}",
+//                                Toast.LENGTH_SHORT
+//                            ).show()
+                        } else
+                            context.requestPermissions()
 //                        viewModel.clear()
 //                        mList.addAll(viewModel.getAllAccounts())
-                        Toast.makeText(
-                            context,
-                            "Активный аккаунт: ${viewModel.newLogin}",
-                            Toast.LENGTH_SHORT
-                        ).show()
                     }, modifier = Modifier
                         .align(Alignment.CenterStart)
                         .padding(10.dp)
@@ -362,9 +366,7 @@ class ProfileFragment : Fragment() {
         )
 
         val name = viewModel.displayName.observeAsState(NameItem("", ""))
-        val status = viewModel.status.observeAsState("")
-
-        val accountStatus = AccountStatus.UNREGISTERED
+        val accountStatus = viewModel.status.observeAsState(AccountStatus.UNREGISTERED)
 
         Column(
             Modifier.fillMaxWidth(),
@@ -400,7 +402,7 @@ class ProfileFragment : Fragment() {
             ) {
                 Image(
                     painter = painterResource(id = R.drawable.logo_mephi),
-                    contentDescription = null
+                    contentDescription = "лого"
                 )
                 Box(modifier = Modifier.size(100.dp)) {
                     Image(
@@ -414,11 +416,11 @@ class ProfileFragment : Fragment() {
                     )
 
                     Icon(
-                        Icons.Filled.CheckCircle, "Status",
-                        tint = when (accountStatus) {
-                            AccountStatus.REGISTERED -> Color.Green
-                            AccountStatus.UNREGISTERED -> Color.Red
-                            AccountStatus.NO_CONNECTION -> Color.Gray
+                        Icons.Filled.CheckCircle, "Статус",
+                        tint = when (accountStatus.value) {
+                            AccountStatus.REGISTERED -> colorResource(id = R.color.colorGreen)
+                            AccountStatus.UNREGISTERED, AccountStatus.REGISTRATION_FAILED -> Color.Red
+                            AccountStatus.NO_CONNECTION, AccountStatus.CHANGING, AccountStatus.LOADING -> Color.Gray
                         },
                         modifier = Modifier
                             .align(Alignment.BottomEnd)
@@ -433,6 +435,24 @@ class ProfileFragment : Fragment() {
                         }
                     }
                 }
+                if (accountStatus.value == AccountStatus.REGISTRATION_FAILED
+                    || accountStatus.value == AccountStatus.UNREGISTERED
+                )
+                    IconButton(
+                        modifier = Modifier
+                            .align(Alignment.Bottom),
+                        onClick = {
+                            viewModel.retryRegistration()
+                        }
+                    ) {
+                        Icon(
+                            painter = painterResource(
+                                id = R.drawable.ic_baseline_update_24
+                            ),
+                            contentDescription = "Обновить",
+                            tint = Color.Red
+                        )
+                    }
             }
 
             Column(
@@ -441,7 +461,7 @@ class ProfileFragment : Fragment() {
                     .padding(20.dp),
                 horizontalAlignment = Alignment.Start
             ) {
-                if (name.value?.display_name?.isNotEmpty() == true)
+                if (!name.value?.display_name.isNullOrEmpty())
                     Text(
                         fontSize = 25.sp,
                         fontWeight = FontWeight.Medium,
@@ -476,7 +496,7 @@ class ProfileFragment : Fragment() {
                         withStyle(style = SpanStyle(color = colorResource(id = R.color.colorAccent))) {
                             append("Статус: ")
                         }
-                        append(status.value)
+                        append(accountStatus.value.status)
                     }
                 )
             }
@@ -496,7 +516,7 @@ class ProfileFragment : Fragment() {
                     modifier = Modifier
                         .align(Alignment.End)
                         .padding(16.dp, 16.dp, 16.dp, 0.dp),
-                    backgroundColor = Color(rgb(76, 175, 80)),
+                    backgroundColor = colorResource(id = R.color.colorGreen),
                     onClick = {
                         openSheet(BottomSheetScreen.ScreenChangeAccount)
 //                    navController.navigate(
@@ -512,7 +532,7 @@ class ProfileFragment : Fragment() {
                             color = Color.White
                         )
                     },
-                    backgroundColor = Color(rgb(76, 175, 80)),
+                    backgroundColor = colorResource(id = R.color.colorGreen),
                     modifier = Modifier
                         .align(Alignment.End)
                         .padding(16.dp, 16.dp, 16.dp, 16.dp),

+ 18 - 9
app/src/main/java/ru/mephi/voip/ui/profile/ProfileViewModel.kt

@@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
 import org.abtollc.sdk.OnRegistrationListener
 import org.koin.core.component.KoinComponent
 import org.koin.java.KoinJavaComponent.inject
+import ru.mephi.voip.call.abto.AccountStatus
 import ru.mephi.voip.call.ui.AbtoViewModel
 import ru.mephi.voip.data.model.Account
 import ru.mephi.voip.data.model.NameItem
@@ -35,32 +36,32 @@ class ProfileViewModel(app: Application, override var sp: SharedPreferences) :
     val newLogin: MutableState<String> = mutableStateOf("")
     val newPassword: MutableState<String> = mutableStateOf("")
 
-    private var _displayName = MutableLiveData<NameItem>()
+    private var _displayName = MutableLiveData<NameItem?>()
     val displayName: LiveData<NameItem?> get() = _displayName
 
-    private var _status = MutableLiveData("")
-    val status: LiveData<String> get() = _status
+    private var _status = MutableLiveData(AccountStatus.UNREGISTERED)
+    val status: LiveData<AccountStatus> get() = _status
 
     init {
         abtoPhone.setNetworkEventListener { connected, networkType ->
             if (!connected)
-                _status.postValue("Сеть недоступна")
+                _status.postValue(AccountStatus.NO_CONNECTION)
             else
-                _status.postValue("Подключение...")
+                _status.postValue(AccountStatus.LOADING)
         }
 
         abtoPhone.setRegistrationStateListener(object : OnRegistrationListener {
             override fun onRegistered(accId: Long) {
-                _status.postValue("Аккаунт активен")
+                _status.postValue(AccountStatus.REGISTERED)
                 fetchName()
             }
 
             override fun onUnRegistered(accId: Long) {
-                _status.postValue("Аккаунт неактивен")
+                _status.postValue(AccountStatus.UNREGISTERED)
             }
 
             override fun onRegistrationFailed(accId: Long, statusCode: Int, statusText: String?) {
-                _status.postValue("Ошибка регистрации")
+                _status.postValue(AccountStatus.REGISTRATION_FAILED)
             }
         })
     }
@@ -125,9 +126,16 @@ class ProfileViewModel(app: Application, override var sp: SharedPreferences) :
         saveAccounts(list)
     }
 
+    fun retryRegistration() {
+        _status.postValue(AccountStatus.CHANGING)
+        getActiveAccount()?.let {
+            updateActiveAccount(it)
+        }
+    }
+
     fun updateActiveAccount(account: Account): String {
         val list = getAllAccounts()
-        _status.postValue("Смена аккаунта...")
+        _status.postValue(AccountStatus.CHANGING)
 
         list.forEach { it.isActive = false }
         list.forEach {
@@ -149,6 +157,7 @@ class ProfileViewModel(app: Application, override var sp: SharedPreferences) :
             true
         )
 
+        abtoPhone.config.registerTimeout = 3000
         abtoPhone.restartSip()
 
         fetchName()

+ 10 - 0
app/src/main/res/drawable/ic_baseline_update_24.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
+</vector>

+ 1 - 0
app/src/main/res/values/colors.xml

@@ -5,4 +5,5 @@
     <color name="colorPrimaryDark">#00BBEE</color>
     <color name="colorAccent">#FF5000</color>
     <color name="colorGray">#878483</color>
+    <color name="colorGreen">#4CAF50</color>
 </resources>