Started to work on better permissions handling

This commit is contained in:
Juha Heinanen
2021-11-09 15:15:46 +02:00
parent b79d10770c
commit 801c232793
10 changed files with 467 additions and 314 deletions

View File

@ -57,4 +57,6 @@ dependencies {
implementation "androidx.core:core-ktx:1.7.0" implementation "androidx.core:core-ktx:1.7.0"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.3.6"
} }

View File

@ -41,6 +41,7 @@
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:remove="android:appComponentFactory"
tools:targetApi="q"> tools:targetApi="q">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@ -21,6 +21,7 @@ import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import android.content.Intent import android.content.Intent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.pm.PackageManager
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.annotation.StringRes import androidx.annotation.StringRes
@ -36,6 +37,7 @@ import kotlin.concurrent.schedule
import kotlin.math.roundToInt import kotlin.math.roundToInt
import android.media.MediaPlayer import android.media.MediaPlayer
import android.telecom.TelecomManager import android.telecom.TelecomManager
import androidx.core.app.ActivityCompat
class BaresipService: Service() { class BaresipService: Service() {
@ -576,20 +578,23 @@ class BaresipService: Service() {
"call incoming" -> { "call incoming" -> {
val peerUri = Api.call_peeruri(callp) val peerUri = Api.call_peeruri(callp)
if (Call.calls().size > 0 || if (Call.calls().size > 0 ||
(Utils.checkPermission(this, Manifest.permission.READ_PHONE_STATE) && !Utils.checkPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO)) ||
tm.isInCall) || ActivityCompat.checkSelfPermission(
!Utils.checkPermission(applicationContext, Manifest.permission.RECORD_AUDIO)) { this,
Log.d(TAG, "Auto-rejecting incoming call $uap/$callp/$peerUri") Manifest.permission.READ_PHONE_STATE
Api.ua_hangup(uap, callp, 486, "Busy Here") ) != PackageManager.PERMISSION_GRANTED ||
if (ua.account.callHistory) { tm.isInCall) {
CallHistory.add(CallHistory(aor, peerUri, "in", false)) Log.d(TAG, "Auto-rejecting incoming call $uap/$callp/$peerUri")
CallHistory.save() Api.ua_hangup(uap, callp, 486, "Busy Here")
ua.account.missedCalls = true if (ua.account.callHistory) {
} CallHistory.add(CallHistory(aor, peerUri, "in", false))
playUnInterrupted(R.raw.callwaiting, 1) CallHistory.save()
if (!Utils.isVisible()) ua.account.missedCalls = true
return }
newEvent = "call rejected" playUnInterrupted(R.raw.callwaiting, 1)
if (!Utils.isVisible())
return
newEvent = "call rejected"
} else { } else {
Log.d(TAG, "Incoming call $uap/$callp/$peerUri") Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
Call(callp, ua, peerUri, "in", "incoming", Call(callp, ua, peerUri, "in", "incoming",

View File

@ -12,10 +12,15 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import android.widget.AdapterView import android.widget.AdapterView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar
import com.tutpro.baresip.Utils.copyInputStreamToFile import com.tutpro.baresip.Utils.copyInputStreamToFile
import com.tutpro.baresip.Utils.showSnackBar
import com.tutpro.baresip.databinding.ActivityConfigBinding import com.tutpro.baresip.databinding.ActivityConfigBinding
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -24,6 +29,7 @@ import java.util.*
class ConfigActivity : AppCompatActivity() { class ConfigActivity : AppCompatActivity() {
private lateinit var binding: ActivityConfigBinding private lateinit var binding: ActivityConfigBinding
private lateinit var layout: ScrollView
private lateinit var autoStart: CheckBox private lateinit var autoStart: CheckBox
private lateinit var listenAddr: EditText private lateinit var listenAddr: EditText
private lateinit var dnsServers: EditText private lateinit var dnsServers: EditText
@ -34,6 +40,7 @@ class ConfigActivity : AppCompatActivity() {
private lateinit var debug: CheckBox private lateinit var debug: CheckBox
private lateinit var sipTrace: CheckBox private lateinit var sipTrace: CheckBox
private lateinit var reset: CheckBox private lateinit var reset: CheckBox
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private var oldAutoStart = "" private var oldAutoStart = ""
private var oldListenAddr = "" private var oldListenAddr = ""
@ -54,6 +61,7 @@ class ConfigActivity : AppCompatActivity() {
binding = ActivityConfigBinding.inflate(layoutInflater) binding = ActivityConfigBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
layout = binding.ConfigView
Utils.addActivity("config") Utils.addActivity("config")
@ -88,8 +96,9 @@ class ConfigActivity : AppCompatActivity() {
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK) {
it.data?.data?.also { uri -> it.data?.data?.also { uri ->
try { try {
val inputStream = applicationContext.contentResolver.openInputStream(uri) val inputStream =
as FileInputStream applicationContext.contentResolver.openInputStream(uri)
as FileInputStream
File(BaresipService.filesPath + "/cert.pem") File(BaresipService.filesPath + "/cert.pem")
.copyInputStreamToFile(inputStream) .copyInputStreamToFile(inputStream)
inputStream.close() inputStream.close()
@ -113,26 +122,45 @@ class ConfigActivity : AppCompatActivity() {
certificateFile.setOnCheckedChangeListener { _, isChecked -> certificateFile.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) { if (isChecked) {
if (Build.VERSION.SDK_INT < 29) { if (Build.VERSION.SDK_INT < 29) {
if (!Utils.requestPermission(this, certificateFile.isChecked = false
Manifest.permission.READ_EXTERNAL_STORAGE, if (ContextCompat.checkSelfPermission(
READ_CERT_PERMISSION_CODE)) { this,
certificateFile.isChecked = false Manifest.permission.READ_EXTERNAL_STORAGE
return@setOnCheckedChangeListener ) == PackageManager.PERMISSION_GRANTED
) {
Log.d(TAG, "Read External Storage permission granted")
val downloadsPath = Utils.downloadsPath("cert.pem")
val content = Utils.getFileContents(downloadsPath)
if (content == null) {
Utils.alertView(
this, getString(R.string.error),
getString(R.string.read_cert_error)
)
return@setOnCheckedChangeListener
}
val filesPath = BaresipService.filesPath + "/cert.pem"
Utils.putFileContents(filesPath, content)
Config.removeVariable("sip_certificate")
Config.addLine("sip_certificate $filesPath")
certificateFile.isChecked = true
save = true
restart = true
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
)
) {
layout.showSnackBar(
binding.root,
getString(R.string.no_restore),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
} else {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} }
val downloadsPath = Utils.downloadsPath("cert.pem")
val content = Utils.getFileContents(downloadsPath)
if (content == null) {
Utils.alertView(this, getString(R.string.error),
getString(R.string.read_cert_error))
certificateFile.isChecked = false
return@setOnCheckedChangeListener
}
val filesPath = BaresipService.filesPath + "/cert.pem"
Utils.putFileContents(filesPath, content)
Config.removeVariable("sip_certificate")
Config.addLine("sip_certificate $filesPath")
save = true
restart = true
} else { } else {
Utils.selectInputFile(certificateRequest) Utils.selectInputFile(certificateRequest)
} }
@ -157,8 +185,9 @@ class ConfigActivity : AppCompatActivity() {
if (it.resultCode == Activity.RESULT_OK) if (it.resultCode == Activity.RESULT_OK)
it.data?.data?.also { uri -> it.data?.data?.also { uri ->
try { try {
val inputStream = applicationContext.contentResolver.openInputStream(uri) val inputStream =
as FileInputStream applicationContext.contentResolver.openInputStream(uri)
as FileInputStream
File(BaresipService.filesPath + "/ca_certs.crt") File(BaresipService.filesPath + "/ca_certs.crt")
.copyInputStreamToFile(inputStream) .copyInputStreamToFile(inputStream)
inputStream.close() inputStream.close()
@ -167,8 +196,10 @@ class ConfigActivity : AppCompatActivity() {
save = true save = true
restart = true restart = true
} catch (e: Error) { } catch (e: Error) {
Utils.alertView(this, getString(R.string.error), Utils.alertView(
getString(R.string.read_ca_certs_error)) this, getString(R.string.error),
getString(R.string.read_ca_certs_error)
)
caFile.isChecked = false caFile.isChecked = false
} }
} }
@ -179,26 +210,45 @@ class ConfigActivity : AppCompatActivity() {
caFile.setOnCheckedChangeListener { _, isChecked -> caFile.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) { if (isChecked) {
if (Build.VERSION.SDK_INT < 29) { if (Build.VERSION.SDK_INT < 29) {
if (!Utils.requestPermission(this, caFile.isChecked = false
Manifest.permission.READ_EXTERNAL_STORAGE, if (ContextCompat.checkSelfPermission(
READ_CERT_PERMISSION_CODE)) { this,
caFile.isChecked = false Manifest.permission.READ_EXTERNAL_STORAGE
return@setOnCheckedChangeListener ) == PackageManager.PERMISSION_GRANTED
) {
Log.d(TAG, "Read External Storage permission granted")
val downloadsPath = Utils.downloadsPath("ca_certs.crt")
val content = Utils.getFileContents(downloadsPath)
if (content == null) {
Utils.alertView(
this, getString(R.string.error),
getString(R.string.read_ca_certs_error)
)
return@setOnCheckedChangeListener
}
val filesPath = BaresipService.filesPath + "/ca_certs.crt"
Utils.putFileContents(filesPath, content)
Config.removeVariable("sip_cafile")
Config.addLine("sip_cafile $filesPath")
caFile.isChecked = true
save = true
restart = true
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
)
) {
layout.showSnackBar(
binding.root,
getString(R.string.no_restore),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
} else {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} }
val downloadsPath = Utils.downloadsPath("ca_certs.crt")
val content = Utils.getFileContents(downloadsPath)
if (content == null) {
Utils.alertView(this, getString(R.string.error),
getString(R.string.read_ca_certs_error))
caFile.isChecked = false
return@setOnCheckedChangeListener
}
val filesPath = BaresipService.filesPath + "/ca_certs.crt"
Utils.putFileContents(filesPath, content)
Config.removeVariable("sip_cafile")
Config.addLine("sip_cafile $filesPath")
save = true
restart = true
} else { } else {
Utils.selectInputFile(certificatesRequest) Utils.selectInputFile(certificatesRequest)
} }
@ -218,14 +268,22 @@ class ConfigActivity : AppCompatActivity() {
volVals.removeAt(curVal) volVals.removeAt(curVal)
volKeys.add(0, curKey) volKeys.add(0, curKey)
volVals.add(0, curVal) volVals.add(0, curVal)
val callVolAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, val callVolAdapter = ArrayAdapter(
volKeys) this, android.R.layout.simple_spinner_item,
volKeys
)
callVolAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) callVolAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
callVolSpinner.adapter = callVolAdapter callVolSpinner.adapter = callVolAdapter
callVolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { callVolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { override fun onItemSelected(
parent: AdapterView<*>,
view: View?,
position: Int,
id: Long
) {
callVolume = volVals[volKeys.indexOf(parent.selectedItem.toString())] callVolume = volVals[volKeys.indexOf(parent.selectedItem.toString())]
} }
override fun onNothingSelected(parent: AdapterView<*>) { override fun onNothingSelected(parent: AdapterView<*>) {
} }
} }
@ -240,7 +298,7 @@ class ConfigActivity : AppCompatActivity() {
"2" "2"
else else
dbCv[0] dbCv[0]
debug.isChecked = oldLogLevel == "0" debug.isChecked = oldLogLevel == "0"
sipTrace = binding.SipTrace sipTrace = binding.SipTrace
sipTrace.isChecked = BaresipService.sipTrace sipTrace.isChecked = BaresipService.sipTrace
@ -252,7 +310,7 @@ class ConfigActivity : AppCompatActivity() {
if (isChecked) { if (isChecked) {
val titleView = View.inflate(this, R.layout.alert_title, null) as TextView val titleView = View.inflate(this, R.layout.alert_title, null) as TextView
titleView.text = getString(R.string.confirmation) titleView.text = getString(R.string.confirmation)
with (AlertDialog.Builder(this@ConfigActivity)) { with(AlertDialog.Builder(this@ConfigActivity)) {
setCustomTitle(titleView) setCustomTitle(titleView)
setMessage(getString(R.string.reset_config_alert)) setMessage(getString(R.string.reset_config_alert))
setPositiveButton(getText(R.string.reset)) { dialog, _ -> setPositiveButton(getText(R.string.reset)) { dialog, _ ->
@ -275,6 +333,12 @@ class ConfigActivity : AppCompatActivity() {
} }
override fun onStart() {
super.onStart()
requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
@ -413,28 +477,6 @@ class ConfigActivity : AppCompatActivity() {
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
READ_CERT_PERMISSION_CODE ->
if (grantResults.isNotEmpty() &&
(grantResults[0] == PackageManager.PERMISSION_GRANTED))
menu!!.performIdentifierAction(R.id.checkIcon, 0)
else
certificateFile.isChecked = false
READ_CA_PERMISSION_CODE ->
if (grantResults.isNotEmpty() &&
(grantResults[0] == PackageManager.PERMISSION_GRANTED))
menu!!.performIdentifierAction(R.id.checkIcon, 0)
else
caFile.isChecked = false
}
}
override fun onBackPressed() { override fun onBackPressed() {
BaresipService.activities.remove("config") BaresipService.activities.remove("config")

View File

@ -7,10 +7,11 @@ const val HIGH_CHANNEL_ID = "com.tutpro.baresip.high"
const val BACKUP_PERMISSION_REQUEST_CODE = 1 const val BACKUP_PERMISSION_REQUEST_CODE = 1
const val RESTORE_PERMISSION_REQUEST_CODE = 2 const val RESTORE_PERMISSION_REQUEST_CODE = 2
const val RECORD_PERMISSION_REQUEST_CODE = 3 const val CALL_PERMISSION_REQUEST_CODE = 3
const val CONTACTS_PERMISSION_REQUEST_CODE = 4 const val CONTACT_PERMISSION_REQUEST_CODE = 4
const val READ_CERT_PERMISSION_CODE = 5 const val READ_CERT_PERMISSION_CODE = 5
const val READ_CA_PERMISSION_CODE = 6 const val READ_CA_PERMISSION_CODE = 6
const val REQUEST_PERMISSIONS_CODE = 7
const val STATUS_NOTIFICATION_ID = 101 const val STATUS_NOTIFICATION_ID = 101
const val CALL_NOTIFICATION_ID = 102 const val CALL_NOTIFICATION_ID = 102

View File

@ -12,6 +12,7 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.ContactsContract import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds import android.provider.ContactsContract.CommonDataKinds
@ -20,10 +21,14 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.google.android.material.snackbar.Snackbar
import com.tutpro.baresip.Utils.showSnackBar
import com.tutpro.baresip.databinding.ActivityContactBinding import com.tutpro.baresip.databinding.ActivityContactBinding
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
@ -31,6 +36,7 @@ import java.io.File
class ContactActivity : AppCompatActivity() { class ContactActivity : AppCompatActivity() {
private lateinit var binding: ActivityContactBinding private lateinit var binding: ActivityContactBinding
private lateinit var layout: LinearLayout
private lateinit var textAvatarView: TextView private lateinit var textAvatarView: TextView
private lateinit var cardAvatarView: CardView private lateinit var cardAvatarView: CardView
private lateinit var cardImageAvatarView: ImageView private lateinit var cardImageAvatarView: ImageView
@ -38,6 +44,7 @@ class ContactActivity : AppCompatActivity() {
private lateinit var uriView: EditText private lateinit var uriView: EditText
private lateinit var androidCheck: CheckBox private lateinit var androidCheck: CheckBox
private lateinit var menu: Menu private lateinit var menu: Menu
private lateinit var requestPermissionsLauncher: ActivityResultLauncher<Array<String>>
private var newContact = false private var newContact = false
private var newAvatar = "" private var newAvatar = ""
@ -48,11 +55,15 @@ class ContactActivity : AppCompatActivity() {
private var id: Long = 0 private var id: Long = 0
private var oldAndroid = false private var oldAndroid = false
private val permissions =
arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityContactBinding.inflate(layoutInflater) binding = ActivityContactBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
layout = binding.ContactView
textAvatarView = binding.TextAvatar textAvatarView = binding.TextAvatar
cardAvatarView = binding.CardAvatar cardAvatarView = binding.CardAvatar
@ -166,10 +177,22 @@ class ContactActivity : AppCompatActivity() {
getString(R.string.android_contact_help)) getString(R.string.android_contact_help))
} }
androidCheck.setOnClickListener{
if (Build.VERSION.SDK_INT >= 23)
if (!Utils.checkPermissions(this, permissions))
requestPermissions(permissions, CONTACT_PERMISSION_REQUEST_CODE)
}
Utils.addActivity("contact,$newContact,$uOrI") Utils.addActivity("contact,$newContact,$uOrI")
} }
override fun onStart() {
super.onStart()
requestPermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {}
}
override fun onCreateOptionsMenu(optionsMenu: Menu): Boolean { override fun onCreateOptionsMenu(optionsMenu: Menu): Boolean {
super.onCreateOptionsMenu(optionsMenu) super.onCreateOptionsMenu(optionsMenu)
@ -181,6 +204,47 @@ class ContactActivity : AppCompatActivity() {
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grandResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grandResults)
var allowed = true
when (requestCode) {
CONTACT_PERMISSION_REQUEST_CODE ->
for (res in grandResults)
allowed = allowed && res == PackageManager.PERMISSION_GRANTED
}
if (!allowed) {
androidCheck.isChecked = oldAndroid
if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.READ_CONTACTS
))
{
layout.showSnackBar(
binding.root,
getString(R.string.no_android_contacts),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionsLauncher.launch(permissions)
}
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.WRITE_CONTACTS
))
{
layout.showSnackBar(
binding.root,
getString(R.string.no_android_contacts),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionsLauncher.launch(permissions)
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (BaresipService.activities.indexOf("contact,$newContact,$uOrI") == -1) return true if (BaresipService.activities.indexOf("contact,$newContact,$uOrI") == -1) return true
@ -260,20 +324,13 @@ class ContactActivity : AppCompatActivity() {
Contact.contacts().sortBy { Contact -> Contact.name } Contact.contacts().sortBy { Contact -> Contact.name }
if (Utils.checkPermission(this, Manifest.permission.READ_CONTACTS + if (Utils.checkPermissions(this, permissions)) {
"|" + Manifest.permission.WRITE_CONTACTS)) {
if (contact.androidContact) if (contact.androidContact)
addOrUpdateAndroidContact(this, contact) addOrUpdateAndroidContact(this, contact)
else else if (oldAndroid)
if (oldAndroid) deleteAndroidContact(this, contact)
deleteAndroidContact(this, contact)
} else { } else {
if (contact.androidContact) { contact.androidContact = oldAndroid
Utils.requestPermission(this, Manifest.permission.READ_CONTACTS +
"|" + Manifest.permission.WRITE_CONTACTS,
CONTACTS_PERMISSION_REQUEST_CODE)
return false
}
} }
Contact.save() Contact.save()
@ -298,23 +355,6 @@ class ContactActivity : AppCompatActivity() {
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
CONTACTS_PERMISSION_REQUEST_CODE ->
if (grantResults.isNotEmpty() && permissions.size == grantResults.size) {
for (res in grantResults)
if (res != PackageManager.PERMISSION_GRANTED)
return
menu.performIdentifierAction(R.id.checkIcon, 0)
}
}
}
override fun onBackPressed() { override fun onBackPressed() {
BaresipService.activities.remove("contact,$newContact,$uOrI") BaresipService.activities.remove("contact,$newContact,$uOrI")

View File

@ -128,7 +128,7 @@ class ContactListAdapter(private val ctx: Context, private val rows: ArrayList<C
} }
} }
if (contact.androidContact && if (contact.androidContact &&
Utils.checkPermission(ctx, Manifest.permission.WRITE_CONTACTS)) Utils.checkPermissions(ctx, arrayOf(Manifest.permission.WRITE_CONTACTS)))
ContactActivity.deleteAndroidContact(ctx, contact) ContactActivity.deleteAndroidContact(ctx, contact)
Contact.contacts().removeAt(position) Contact.contacts().removeAt(position)
Contact.save() Contact.save()

View File

@ -29,8 +29,12 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import com.tutpro.baresip.Utils.showSnackBar
import com.tutpro.baresip.databinding.ActivityMainBinding import com.tutpro.baresip.databinding.ActivityMainBinding
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -72,6 +76,8 @@ class MainActivity : AppCompatActivity() {
private lateinit var stopState: String private lateinit var stopState: String
private var speakerIcon: MenuItem? = null private var speakerIcon: MenuItem? = null
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private lateinit var requestPermissionsLauncher: ActivityResultLauncher<Array<String>>
private lateinit var accountsRequest: ActivityResultLauncher<Intent> private lateinit var accountsRequest: ActivityResultLauncher<Intent>
private lateinit var chatRequests: ActivityResultLauncher<Intent> private lateinit var chatRequests: ActivityResultLauncher<Intent>
@ -304,44 +310,10 @@ class MainActivity : AppCompatActivity() {
callButton.setOnClickListener { callButton.setOnClickListener {
if (aorSpinner.selectedItemPosition == -1) if (aorSpinner.selectedItemPosition == -1)
return@setOnClickListener return@setOnClickListener
callUri.setAdapter(null) val permissions = arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_PHONE_STATE)
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition] if (Build.VERSION.SDK_INT >= 23)
val aor = ua.account.aor if (!Utils.checkPermissions(this, permissions))
if (Call.calls().isEmpty()) { requestPermissions(permissions, CALL_PERMISSION_REQUEST_CODE)
val uriText = callUri.text.toString().trim()
if (uriText.isNotEmpty()) {
val uri = Utils.uriComplete(
ContactsActivity.findContactURI(uriText).filterNot { it.isWhitespace() },
Utils.aorDomain(aor)
)
if (!Utils.checkSipUri(uri)) {
Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.invalid_sip_uri), uri))
} else {
callUri.isFocusable = false
if (!call(ua, uri)) {
callButton.visibility = View.VISIBLE
callButton.isEnabled = true
hangupButton.visibility = View.INVISIBLE
hangupButton.isEnabled = false
} else {
callButton.visibility = View.INVISIBLE
callButton.isEnabled = false
hangupButton.visibility = View.VISIBLE
hangupButton.isEnabled = true
}
}
} else {
val latest = CallHistory.aorLatestHistory(aor)
if (latest != null)
callUri.setText(
Utils.friendlyUri(
ContactsActivity.contactName(latest.peerUri),
Utils.aorDomain(ua.account.aor)
)
)
}
}
} }
hangupButton.setOnClickListener { hangupButton.setOnClickListener {
@ -656,17 +628,182 @@ class MainActivity : AppCompatActivity() {
delegate.applyDayNight() delegate.applyDayNight()
} }
window.decorView.post { } // OnCreate
if (firstRun) {
if (!Utils.checkPermission(this, Manifest.permission.RECORD_AUDIO)) override fun onStart() {
Utils.requestPermission( Log.i(TAG, "Main onStart")
this, Manifest.permission.RECORD_AUDIO, RECORD_PERMISSION_REQUEST_CODE super.onStart()
) requestPermissionLauncher =
firstRun = false registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
requestPermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
permissions ->
permissions.entries.forEach {
if (it.value)
Log.i(TAG, "Permission ${it.key} granted")
else
Log.i(TAG, "Permission ${it.key} denied")
}
}
}
override fun onResume() {
super.onResume()
Log.d(TAG, "Main onResume with action '$resumeAction'")
nm.cancelAll()
BaresipService.isMainVisible = true
when (resumeAction) {
"call show" ->
handleServiceEvent("call incoming",
arrayListOf(resumeCall!!.ua.uap, resumeCall!!.callp))
"call answer" -> {
answerButton.performClick()
showCall(resumeCall!!.ua)
}
"call missed" -> {
callsButton.performClick()
}
"call reject" ->
rejectButton.performClick()
"call" -> {
callUri.setText(UserAgent.uas()[aorSpinner.selectedItemPosition].account.resumeUri)
callButton.performClick()
}
"transfer show", "transfer accept" ->
handleServiceEvent("$resumeAction,$resumeUri",
arrayListOf(resumeCall!!.ua.uap, resumeCall!!.callp))
"message", "message show", "message reply" ->
handleServiceEvent(resumeAction, arrayListOf(resumeUap, resumeUri))
else -> {
val incomingCall = Call.call("incoming")
if (incomingCall != null) {
spinToAor(incomingCall.ua.account.aor)
} else {
restoreActivities()
if (UserAgent.uas().size > 0) {
if (aorSpinner.selectedItemPosition == -1) {
if (Call.calls().size > 0)
spinToAor(Call.calls()[0].ua.account.aor)
else {
aorSpinner.setSelection(0)
aorSpinner.tag = UserAgent.uas()[0].account.aor
}
}
}
}
uaAdapter.notifyDataSetChanged()
if (UserAgent.uas().size > 0) {
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
showCall(ua)
updateIcons(ua.account)
}
} }
} }
resumeAction = ""
}
} // OnCreate override fun onPause() {
Log.d(TAG, "Main onPause")
Utils.addActivity("main")
BaresipService.isMainVisible = false
saveCallUri()
super.onPause()
}
override fun onStop() {
Log.d(TAG, "Main onStop")
super.onStop()
}
override fun onDestroy() {
Log.d(TAG, "Main onDestroy")
LocalBroadcastManager.getInstance(this).unregisterReceiver(serviceEventReceiver)
BaresipService.activities.clear()
super.onDestroy()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grandResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grandResults)
var allowed = true
when (requestCode) {
CALL_PERMISSION_REQUEST_CODE -> {
for (res in grandResults)
allowed = allowed && res == PackageManager.PERMISSION_GRANTED
if (allowed) {
callUri.setAdapter(null)
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
val aor = ua.account.aor
if (Call.calls().isEmpty()) {
val uriText = callUri.text.toString().trim()
if (uriText.isNotEmpty()) {
val uri = Utils.uriComplete(
ContactsActivity.findContactURI(uriText)
.filterNot { it.isWhitespace() },
Utils.aorDomain(aor)
)
if (!Utils.checkSipUri(uri)) {
Utils.alertView(
this, getString(R.string.notice),
String.format(getString(R.string.invalid_sip_uri), uri)
)
} else {
callUri.isFocusable = false
if (!call(ua, uri)) {
callButton.visibility = View.VISIBLE
callButton.isEnabled = true
hangupButton.visibility = View.INVISIBLE
hangupButton.isEnabled = false
} else {
callButton.visibility = View.INVISIBLE
callButton.isEnabled = false
hangupButton.visibility = View.VISIBLE
hangupButton.isEnabled = true
}
}
} else {
val latest = CallHistory.aorLatestHistory(aor)
if (latest != null)
callUri.setText(
Utils.friendlyUri(
ContactsActivity.contactName(latest.peerUri),
Utils.aorDomain(ua.account.aor)
)
)
}
}
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.RECORD_AUDIO
)
) {
layout.showSnackBar(
binding.root,
getString(R.string.no_android_contacts),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionsLauncher.launch(permissions)
}
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.READ_PHONE_STATE
)
) {
layout.showSnackBar(
binding.root,
getString(R.string.no_android_contacts),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionsLauncher.launch(permissions)
}
}
}
}
}
}
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
// Called when MainActivity already exists at the top of current task // Called when MainActivity already exists at the top of current task
@ -790,84 +927,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
override fun onStart() {
Log.d(TAG, "Main onStart")
super.onStart()
}
override fun onResume() {
super.onResume()
Log.d(TAG, "Main onResume with action '$resumeAction'")
nm.cancelAll()
BaresipService.isMainVisible = true
when (resumeAction) {
"call show" ->
handleServiceEvent("call incoming",
arrayListOf(resumeCall!!.ua.uap, resumeCall!!.callp))
"call answer" -> {
answerButton.performClick()
showCall(resumeCall!!.ua)
}
"call missed" -> {
callsButton.performClick()
}
"call reject" ->
rejectButton.performClick()
"call" -> {
callUri.setText(UserAgent.uas()[aorSpinner.selectedItemPosition].account.resumeUri)
callButton.performClick()
}
"transfer show", "transfer accept" ->
handleServiceEvent("$resumeAction,$resumeUri",
arrayListOf(resumeCall!!.ua.uap, resumeCall!!.callp))
"message", "message show", "message reply" ->
handleServiceEvent(resumeAction, arrayListOf(resumeUap, resumeUri))
else -> {
val incomingCall = Call.call("incoming")
if (incomingCall != null) {
spinToAor(incomingCall.ua.account.aor)
} else {
restoreActivities()
if (UserAgent.uas().size > 0) {
if (aorSpinner.selectedItemPosition == -1) {
if (Call.calls().size > 0)
spinToAor(Call.calls()[0].ua.account.aor)
else {
aorSpinner.setSelection(0)
aorSpinner.tag = UserAgent.uas()[0].account.aor
}
}
}
}
uaAdapter.notifyDataSetChanged()
if (UserAgent.uas().size > 0) {
val ua = UserAgent.uas()[aorSpinner.selectedItemPosition]
showCall(ua)
updateIcons(ua.account)
}
}
}
resumeAction = ""
}
override fun onPause() {
Log.d(TAG, "Main onPause")
Utils.addActivity("main")
BaresipService.isMainVisible = false
saveCallUri()
super.onPause()
}
override fun onStop() {
Log.d(TAG, "Main onStop")
super.onStop()
}
override fun recreate() {
Log.d(TAG, "Main onCreate")
super.recreate()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val stream = if (am.mode == AudioManager.MODE_RINGTONE) val stream = if (am.mode == AudioManager.MODE_RINGTONE)
AudioManager.STREAM_RING AudioManager.STREAM_RING
@ -971,10 +1030,6 @@ class MainActivity : AppCompatActivity() {
} }
"call incoming" -> { "call incoming" -> {
val callp = params[1] val callp = params[1]
if (!Utils.checkPermission(this, Manifest.permission.RECORD_AUDIO)) {
Api.ua_hangup(uap, callp, 486, "Busy Here")
return
}
if (BaresipService.isMainVisible) { if (BaresipService.isMainVisible) {
if (aor != aorSpinner.tag) if (aor != aorSpinner.tag)
spinToAor(aor) spinToAor(aor)
@ -1173,13 +1228,6 @@ class MainActivity : AppCompatActivity() {
moveTaskToBack(true) moveTaskToBack(true)
} }
override fun onDestroy() {
Log.d(TAG, "Main onDestroy")
LocalBroadcastManager.getInstance(this).unregisterReceiver(serviceEventReceiver)
BaresipService.activities.clear()
super.onDestroy()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, menu) menuInflater.inflate(R.menu.main_menu, menu)
menuInflater.inflate(R.menu.speaker_icon, menu) menuInflater.inflate(R.menu.speaker_icon, menu)
@ -1219,24 +1267,59 @@ class MainActivity : AppCompatActivity() {
if (Build.VERSION.SDK_INT >= 29) { if (Build.VERSION.SDK_INT >= 29) {
pickupFileFromDownloads("backup") pickupFileFromDownloads("backup")
} else { } else {
if (Utils.requestPermission( if (ContextCompat.checkSelfPermission(
this, this,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE
BACKUP_PERMISSION_REQUEST_CODE ) == PackageManager.PERMISSION_GRANTED) {
) Log.d(TAG, "Write External Storage permission granted")
) {
val path = Utils.downloadsPath("baresip.bs") val path = Utils.downloadsPath("baresip.bs")
downloadsOutputStream = FileOutputStream(File(path)) downloadsOutputStream = FileOutputStream(File(path))
askPassword(getString(R.string.encrypt_password)) askPassword(getString(R.string.encrypt_password))
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
layout.showSnackBar(
binding.root,
getString(R.string.no_backup),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
} else {
requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} }
} }
} }
R.id.restore -> { R.id.restore -> {
if (Build.VERSION.SDK_INT >= 29 || if (Build.VERSION.SDK_INT >= 29) {
Utils.requestPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE, pickupFileFromDownloads("restore")
RESTORE_PERMISSION_REQUEST_CODE)) } else {
startRestore() if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Read External Storage permission granted")
val path = Utils.downloadsPath("baresip.bs")
downloadsInputStream = FileInputStream(File(path))
askPassword(getString(R.string.decrypt_password))
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.READ_EXTERNAL_STORAGE)) {
layout.showSnackBar(
binding.root,
getString(R.string.no_restore),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
} else {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
} }
R.id.about -> { R.id.about -> {
@ -1250,38 +1333,6 @@ class MainActivity : AppCompatActivity() {
return true return true
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
RECORD_PERMISSION_REQUEST_CODE ->
if ((grantResults.isNotEmpty()) && (grantResults[0] != PackageManager.PERMISSION_GRANTED))
Utils.alertView(this, getString(R.string.notice),
getString(R.string.no_calls))
BACKUP_PERMISSION_REQUEST_CODE ->
if ((grantResults.isNotEmpty()) && (grantResults[0] == PackageManager.PERMISSION_GRANTED))
askPassword(getString(R.string.encrypt_password))
RESTORE_PERMISSION_REQUEST_CODE ->
if ((grantResults.isNotEmpty()) && (grantResults[0] == PackageManager.PERMISSION_GRANTED))
startRestore()
}
}
private fun startRestore() {
if (Build.VERSION.SDK_INT >= 29) {
pickupFileFromDownloads("restore")
} else {
val path = Utils.downloadsPath("baresip.bs")
downloadsInputStream = FileInputStream(File(path))
askPassword(getString(R.string.decrypt_password))
}
}
@RequiresApi(29) @RequiresApi(29)
private fun pickupFileFromDownloads(action: String) { private fun pickupFileFromDownloads(action: String) {
when (action) { when (action) {
@ -1559,11 +1610,6 @@ class MainActivity : AppCompatActivity() {
} }
private fun call(ua: UserAgent, uri: String): Boolean { private fun call(ua: UserAgent, uri: String): Boolean {
if (!Utils.checkPermission(this, Manifest.permission.RECORD_AUDIO)) {
Toast.makeText(applicationContext, getString(R.string.no_calls),
Toast.LENGTH_LONG).show()
return false
}
if (ua.account.aor != aorSpinner.tag) if (ua.account.aor != aorSpinner.tag)
spinToAor(ua.account.aor) spinToAor(ua.account.aor)
val callp = Api.ua_connect(ua.uap, uri, Api.VIDMODE_OFF) val callp = Api.ua_connect(ua.uap, uri, Api.VIDMODE_OFF)

View File

@ -1,6 +1,5 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -22,10 +21,10 @@ import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import com.google.android.material.snackbar.Snackbar
import java.io.* import java.io.*
import java.lang.reflect.Method import java.lang.reflect.Method
import java.security.SecureRandom import java.security.SecureRandom
@ -320,8 +319,9 @@ object Utils {
val addresses: Enumeration<InetAddress> = iface.inetAddresses val addresses: Enumeration<InetAddress> = iface.inetAddresses
while (addresses.hasMoreElements()) { while (addresses.hasMoreElements()) {
val inetAddress: InetAddress = addresses.nextElement() val inetAddress: InetAddress = addresses.nextElement()
if (!inetAddress.isLoopbackAddress && !inetAddress.isLinkLocalAddress) if (inetAddress.hostAddress != null && !inetAddress.isLoopbackAddress &&
result[inetAddress.hostAddress] = ifName !inetAddress.isLinkLocalAddress)
result[inetAddress.hostAddress!!] = ifName
} }
if (result.isNotEmpty()) return result if (result.isNotEmpty()) return result
} }
@ -356,22 +356,33 @@ object Utils {
} }
} }
fun checkPermission(ctx: Context, permissions: String) : Boolean { fun checkPermissions(ctx: Context, permissions: Array<String>) : Boolean {
for (p in permissions.split("|")) { for (p in permissions) {
if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Permission $p is not granted")
return false return false
} else {
Log.d(TAG, "Permission $p is granted")
}
} }
return true return true
} }
fun requestPermission(ctx: Context, permissions: String, requestCode: Int) : Boolean { fun View.showSnackBar(
val pArray = permissions.split("|").toTypedArray() view: View,
for (p in pArray) msg: String,
if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) { length: Int,
ActivityCompat.requestPermissions(ctx as Activity, pArray, requestCode) actionMessage: CharSequence?,
return false action: (View) -> Unit
} ) {
return true 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) { fun copyAssetToFile(context: Context, asset: String, path: String) {

View File

@ -447,8 +447,13 @@
<string name="restore_failed">Failed to restore application data. Check that you gave correct <string name="restore_failed">Failed to restore application data. Check that you gave correct
password. In Android versions 9 and below, also check Apps → baresip → Permissions → Storage password. In Android versions 9 and below, also check Apps → baresip → Permissions → Storage
and that file \'%1$s\' exists in Download folder.</string> and that file \'%1$s\' exists in Download folder.</string>
<string name="no_calls">You are not able to place or answer calls without Microphone permission.</string> <string name="no_calls">You are not able to make or answer calls without Microphone and
<string name="no_video_calls">Grant \"Camera\" permission to place or answer video calls.</string> Telephone permissions.</string>
<string name="no_backup">You are not able create backup without Storage permission.</string>
<string name="no_android_contacts">You are not able add or remove Android contacts without
Contacts permission.</string>
<string name="no_restore">You are not able restore backup without Storage permission.</string>
<string name="no_video_calls">Grant \"Camera\" permission to make or answer video calls.</string>
<string name="no_cameras">You don\'t have any supported video cameras.</string> <string name="no_cameras">You don\'t have any supported video cameras.</string>
<string name="show_password">Show Password</string> <string name="show_password">Show Password</string>
<string name="no_network">No network connection!</string> <string name="no_network">No network connection!</string>