- Added to main menu possibility to export and import all application date.

- Improved encryption/decryption of exported/imported data.  If accounts
  were exported, they need to be exported again.
- Lots of other implemetation improvements.
This commit is contained in:
Juha Heinanen
2019-10-09 11:48:26 +03:00
parent f40c097766
commit ac26c2256d
19 changed files with 420 additions and 300 deletions

View File

@ -3,7 +3,6 @@ package com.tutpro.baresip
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
@ -12,7 +11,6 @@ import android.app.AlertDialog
import android.view.ViewGroup
import android.view.LayoutInflater
import java.io.File
import java.util.ArrayList
class AccountsActivity : AppCompatActivity() {
@ -20,7 +18,6 @@ class AccountsActivity : AppCompatActivity() {
internal lateinit var alAdapter: AccountListAdapter
internal var aor = ""
internal var password = ""
public override fun onCreate(savedInstanceState: Bundle?) {
@ -69,6 +66,7 @@ class AccountsActivity : AppCompatActivity() {
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -92,7 +90,6 @@ class AccountsActivity : AppCompatActivity() {
if (Utils.requestPermission(this,
android.Manifest.permission.READ_EXTERNAL_STORAGE))
askPassword(getString(R.string.decrypt_password))
}
android.R.id.home -> {
@ -120,7 +117,6 @@ class AccountsActivity : AppCompatActivity() {
}
private fun askPassword(title: String) {
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val builder = AlertDialog.Builder(this)
builder.setTitle(title)
val viewInflated = LayoutInflater.from(this)
@ -130,17 +126,17 @@ class AccountsActivity : AppCompatActivity() {
builder.setView(viewInflated)
builder.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
password = input.text.toString()
val password = input.text.toString()
if (password != "") {
if (title == getString(R.string.encrypt_password)) {
if (exportAccounts(dir, password))
if (exportAccounts(password))
Utils.alertView(this, getString(R.string.info),
getString(R.string.exported_accounts))
else
Utils.alertView(this, getString(R.string.error),
getString(R.string.export_accounts_error))
} else {
if (importAccounts(dir, password))
if (importAccounts(password))
Utils.alertView(this, getString(R.string.info),
getString(R.string.imported_accounts))
else
@ -153,6 +149,7 @@ class AccountsActivity : AppCompatActivity() {
dialog.cancel()
}
builder.show()
}
companion object {
@ -169,30 +166,27 @@ class AccountsActivity : AppCompatActivity() {
fun saveAccounts() {
var accounts = ""
for (a in Account.accounts()) accounts = accounts + a.print() + "\n"
Utils.putFileContents(File(BaresipService.filesPath + "/accounts"), accounts)
Utils.putFileContents(BaresipService.filesPath + "/accounts", accounts.toByteArray())
// Log.d("Baresip", "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'")
}
fun exportAccounts(path: File, password: String): Boolean {
var accounts = ""
for (a in Account.accounts())
accounts = accounts + a.print() + "\n"
return Utils.putFileContents(File(path, "accounts.bs"),
Utils.encrypt(accounts, password))
fun exportAccounts(password: String): Boolean {
val content = Utils.getFileContents("${BaresipService.filesPath}/accounts")
if (content == null) return false
return Utils.encryptToFile("${BaresipService.downloadsPath}/accounts.bs",
content, password)
}
fun importAccounts(path: File, password: String): Boolean {
val content = Utils.getFileContents(File(path, "accounts.bs"))
if (content == "Failed") return false
val accounts = Utils.decrypt(content, password)
if (accounts == "") return false
return Utils.putFileContents(File(BaresipService.filesPath + "/accounts"),
accounts)
fun importAccounts(password: String): Boolean {
val content = Utils.decryptFromFile("${BaresipService.downloadsPath}/accounts.bs",
password)
if (content == null) return false
return Utils.putFileContents("${BaresipService.filesPath}/accounts", content)
}
fun noAccounts(): Boolean {
return Utils.getFileContents(File(BaresipService.filesPath + "/accounts"))
.length == 0
val contents = Utils.getFileContents(BaresipService.filesPath + "/accounts")
return contents == null || contents.size == 0
}
}

View File

@ -15,6 +15,7 @@ import android.view.View
import android.widget.RemoteViews
import android.support.v4.content.LocalBroadcastManager
import android.os.Build
import android.os.Environment
import android.support.v4.app.NotificationCompat.VISIBILITY_PRIVATE
import android.support.v4.content.ContextCompat
import android.provider.Settings
@ -55,6 +56,7 @@ class BaresipService: Service() {
intent.setPackage("com.tutpro.baresip")
filesPath = filesDir.absolutePath
downloadsPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
@ -146,7 +148,7 @@ class BaresipService: Service() {
try {
File(filesPath).mkdirs()
} catch (e: Error) {
Log.e(LOG_TAG, "Failed to create directory: " + e.toString())
Log.e(LOG_TAG, "Failed to create directory: $e")
}
}
for (a in assets) {
@ -172,8 +174,9 @@ class BaresipService: Service() {
}
}
ContactsActivity.restoreContacts(applicationContext.filesDir, "contacts")
Message.restoreMessages()
Contact.restore()
CallHistory.restore()
Message.restore()
Thread(Runnable { baresipStart(filesPath) }).start()
isServiceRunning = true
@ -212,7 +215,7 @@ class BaresipService: Service() {
Api.ua_hangup(call.ua.uap, callp, 486, "Rejected")
if (call.ua.account.callHistory) {
CallHistory.add(CallHistory(aor, peerUri, "in", false))
CallHistory.save(filesPath)
CallHistory.save()
}
}
}
@ -420,7 +423,7 @@ class BaresipService: Service() {
Api.ua_hangup(uap, callp, 486, "Busy Here")
if (ua.account.callHistory) {
CallHistory.add(CallHistory(aor, peerUri, "in", false))
CallHistory.save(filesPath)
CallHistory.save()
ua.account.missedCalls = true
}
if (!Utils.isVisible())
@ -499,7 +502,7 @@ class BaresipService: Service() {
call.onhold = false
if (ua.account.callHistory) {
CallHistory.add(CallHistory(aor, call.peerURI, call.dir, true))
CallHistory.save(filesPath)
CallHistory.save()
call.hasHistory = true
}
if (call.dir == "in") {
@ -589,7 +592,7 @@ class BaresipService: Service() {
calls.remove(call)
if (ua.account.callHistory && !call.hasHistory) {
CallHistory.add(CallHistory(aor, call.peerURI, call.dir, false))
CallHistory.save(filesPath)
CallHistory.save()
if (call.dir == "in") ua.account.missedCalls = true
}
if (Call.calls().size == 0) {
@ -636,7 +639,7 @@ class BaresipService: Service() {
Log.d(LOG_TAG, "Message event for $uap from $peer at $timeStamp")
Message.add(Message(ua.account.aor, peer, text, timeStamp.toLong(),
R.drawable.arrow_down_green, 0, "", true))
Message.saveMessages()
Message.save()
ua.account.unreadMessages = true
if (!Utils.isVisible()) {
val intent = Intent(this, BaresipService::class.java)
@ -930,6 +933,7 @@ class BaresipService: Service() {
var dynDns = false
var filesPath = ""
var downloadsPath = ""
var uas = ArrayList<UserAgent>()
var status = ArrayList<Int>()
var calls = ArrayList<Call>()

View File

@ -45,9 +45,9 @@ class CallHistory(val aor: String, val peerURI: String, val direction: String,
return null
}
fun save(path: String) {
Log.d("Baresip", "Saving history of size ${BaresipService.history.size}")
val file = File(path, "history")
fun save() {
Log.d("Baresip", "Saving call history of size ${BaresipService.history.size}")
val file = File(BaresipService.filesPath, "history")
try {
val fos = FileOutputStream(file)
val oos = ObjectOutputStream(fos)
@ -60,21 +60,19 @@ class CallHistory(val aor: String, val peerURI: String, val direction: String,
}
}
fun restore(path: String) {
val file = File(path + "/history")
if (file.exists()) {
fun restore() {
val file = File(BaresipService.filesPath, "history")
if (file.exists())
try {
val fis = FileInputStream(file)
val ois = ObjectInputStream(fis)
BaresipService.history = ois.readObject() as ArrayList<CallHistory>
ois.close()
fis.close()
Log.d("Baresip", "Restored history of size ${BaresipService.history.size}")
Log.d("Baresip", "Restored call history of size ${BaresipService.history.size}")
} catch (e: Exception) {
Log.e("Baresip", "InputStream exception: - " + e.toString())
}
}
}
fun print() {

View File

@ -127,7 +127,7 @@ class CallsActivity : AppCompatActivity() {
override fun onPause() {
CallHistory.save(applicationContext.filesDir.absolutePath)
CallHistory.save()
super.onPause()
}
@ -142,7 +142,7 @@ class CallsActivity : AppCompatActivity() {
aor.substringAfter(":")))
deleteDialog.setPositiveButton(getText(R.string.delete)) { dialog, _ ->
CallHistory.clear(aor)
CallHistory.save(applicationContext.filesDir.absolutePath)
CallHistory.save()
aorGenerateHistory(aor)
clAdapter.notifyDataSetChanged()
dialog.dismiss()

View File

@ -97,7 +97,7 @@ class ChatActivity : AppCompatActivity() {
if (chatMessages.size == 0) {
listView.removeFooterView(footerView)
}
Message.saveMessages()
Message.save()
}
DialogInterface.BUTTON_NEUTRAL -> {
}
@ -224,7 +224,7 @@ class ChatActivity : AppCompatActivity() {
save = true
}
}
if (save) Message.saveMessages()
if (save) Message.save()
imm.hideSoftInputFromWindow(newMessage.windowToken, 0)
BaresipService.activities.removeAt(0)
val i = Intent()

View File

@ -72,7 +72,7 @@ class ChatsActivity: AppCompatActivity() {
clAdapter.remove(m)
clAdapter.notifyDataSetChanged()
BaresipService.messages = msgs
Message.saveMessages()
Message.save()
uaMessages = uaMessages(aor)
}
DialogInterface.BUTTON_NEUTRAL -> {
@ -159,7 +159,7 @@ class ChatsActivity: AppCompatActivity() {
aor.substringAfter(":")))
deleteDialog.setPositiveButton(getText(R.string.delete)) { dialog, _ ->
Message.clear(aor)
Message.saveMessages()
Message.save()
uaMessages.clear()
clAdapter.notifyDataSetChanged()
Account.findUa(aor)!!.account.unreadMessages = false
@ -227,7 +227,7 @@ class ChatsActivity: AppCompatActivity() {
if ((Message.messages()[i].aor == aor) &&
(Message.messages()[i].timeStamp == time)) {
Message.messages()[i].new = false
Message.saveMessages()
Message.save()
return
}
}
@ -237,7 +237,7 @@ class ChatsActivity: AppCompatActivity() {
if ((Message.messages()[i].aor == aor) &&
(Message.messages()[i].timeStamp == time)) {
Message.messages().removeAt(i)
Message.saveMessages()
Message.save()
return
}
}

View File

@ -1,16 +1,17 @@
package com.tutpro.baresip
import android.content.Context
import java.io.File
import java.net.InetAddress
import java.nio.charset.StandardCharsets
object Config {
private val path = BaresipService.filesPath + "/config"
private val file = File(path)
private var config = Utils.getFileContents(file)
private val configPath = BaresipService.filesPath + "/config"
private var config = String(Utils.getFileContents(configPath)!!, StandardCharsets.ISO_8859_1)
fun initialize(dnsServers: List<InetAddress>) {
Log.d("Baresip", "Config is '$config'")
var write = false
if (!config.contains("zrtp_hash")) {
config = "${config}zrtp_hash yes\n"
@ -41,10 +42,10 @@ object Config {
}
if (!config.contains("opus_samplerate")) {
config = "${config}opus_samplerate 16000\n"
val accountsFile = File(BaresipService.filesPath + "/config")
var accounts = Utils.getFileContents(accountsFile)
val accountsPath = BaresipService.filesPath + "/accounts"
var accounts = String(Utils.getFileContents(accountsPath)!!, StandardCharsets.ISO_8859_1)
accounts = accounts.replace("opus/48000/1", "opus/16000/1")
Utils.putFileContents(accountsFile, accounts)
Utils.putFileContents(accountsPath, accounts.toByteArray())
write = true
}
if (!config.contains("opus_stereo")) {
@ -95,7 +96,7 @@ object Config {
}
if (write) {
Log.e("Baresip", "Writing config '$config'")
Utils.putFileContents(file, config)
Utils.putFileContents(configPath, config.toByteArray())
}
}
@ -115,7 +116,7 @@ object Config {
fun remove(variable: String) {
config = Utils.removeLinesStartingWithName(config, variable)
Utils.putFileContents(file, config)
Utils.putFileContents(configPath, config.toByteArray())
}
fun replace(variable: String, value: String) {
@ -124,7 +125,7 @@ object Config {
}
fun reset(ctx: Context) {
Utils.copyAssetToFile(ctx, "config", path)
Utils.copyAssetToFile(ctx, "config", configPath)
}
fun save() {
@ -133,7 +134,7 @@ object Config {
if (line.length > 0)
result = result + line + '\n'
config = result
Utils.putFileContents(file, config)
Utils.putFileContents(configPath, config.toByteArray())
Log.d("Baresip", "New config '$result'")
// Api.reload_config()
}

View File

@ -6,15 +6,12 @@ import android.content.Intent
import android.net.ConnectivityManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import java.io.File
class ConfigActivity : AppCompatActivity() {
internal lateinit var autoStart: CheckBox
@ -44,8 +41,6 @@ class ConfigActivity : AppCompatActivity() {
private var callVolume = BaresipService.callVolume
private var save = false
private var restart = false
private var downloadsDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
override fun onCreate(savedInstanceState: Bundle?) {
@ -226,14 +221,13 @@ class ConfigActivity : AppCompatActivity() {
if (!Utils.requestPermission(this,
android.Manifest.permission.READ_EXTERNAL_STORAGE))
return false
val content = Utils.getFileContents(File(downloadsDir, "cert.pem"))
if (content == "Failed") {
val content = Utils.getFileContents(BaresipService.downloadsPath + "/cert.pem")
if (content == null) {
Utils.alertView(this, getString(R.string.error),
getString(R.string.read_cert_error))
return false
}
Utils.putFileContents(File(BaresipService.filesPath + "/cert.pem"),
content)
Utils.putFileContents(BaresipService.filesPath + "/cert.pem", content)
Config.remove("sip_certificate")
Config.add("sip_certificate", BaresipService.filesPath + "/cert.pem")
} else {
@ -248,13 +242,13 @@ class ConfigActivity : AppCompatActivity() {
if (!Utils.requestPermission(this,
android.Manifest.permission.READ_EXTERNAL_STORAGE))
return false
val content = Utils.getFileContents(File(downloadsDir, "ca_certs.crt"))
if (content == "Failed") {
val content = Utils.getFileContents(BaresipService.downloadsPath + "/ca_certs.crt")
if (content == null) {
Utils.alertView(this, getString(R.string.error),
getString(R.string.read_ca_certs_error))
return false
}
Utils.putFileContents(File(BaresipService.filesPath + "/ca_certs.crt"),
Utils.putFileContents(BaresipService.filesPath + "/ca_certs.crt",
content)
Config.remove("sip_cafile")
Config.add("sip_cafile", BaresipService.filesPath + "/ca_certs.crt")

View File

@ -1,5 +1,6 @@
package com.tutpro.baresip
import java.nio.charset.StandardCharsets
import java.util.ArrayList
class Contact(var name: String, var uri: String) {
@ -12,5 +13,47 @@ class Contact(var name: String, var uri: String) {
return BaresipService.contacts
}
fun save(): Boolean {
var contents = ""
for (c in BaresipService.contacts) contents += "\"${c.name}\" <${c.uri}>\n"
return Utils.putFileContents(BaresipService.filesPath + "/contacts",
contents.toByteArray())
}
fun restore(): Boolean {
val content = Utils.getFileContents(BaresipService.filesPath + "/contacts")
if (content == null) return false
val contacts = String(content, StandardCharsets.ISO_8859_1)
Api.contacts_remove()
BaresipService.contacts.clear()
contacts.lines().forEach {
val parts = it.split("\"")
if (parts.size == 3) {
val name = parts[1]
var uri = parts[2].trim()
if (uri.startsWith("<"))
uri = uri.substringAfter("<").substringBefore(">")
// Currently no need to make baresip aware of the contact
// Api.contact_add("\"$name\" $uri")
BaresipService.contacts.add(Contact(name, uri))
}
}
return true
}
fun export(): Boolean {
return Utils.putFileContents(BaresipService.downloadsPath + "/contacts.bs",
Utils.getFileContents(BaresipService.filesPath + "/contacts")!!)
}
fun import(): Boolean {
val contacts = Utils.getFileContents(BaresipService.downloadsPath + "/contacts.bs")
if (contacts != null) {
Utils.putFileContents(BaresipService.filesPath + "/contacts", contacts)
return restore()
}
return false
}
}
}

View File

@ -119,7 +119,7 @@ class ContactActivity : AppCompatActivity() {
}
Contact.contacts().sortBy { Contact -> Contact.name }
ContactsActivity.saveContacts(applicationContext.filesDir, "contacts")
Contact.save()
i.putExtra("name", newName)
setResult(Activity.RESULT_OK, i)

View File

@ -67,7 +67,7 @@ class ContactListAdapter(private val cxt: Context, private val rows: ArrayList<C
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
Contact.contacts().removeAt(pos)
ContactsActivity.saveContacts(cxt.applicationContext.filesDir, "contacts")
Contact.save()
this.notifyDataSetChanged()
}
DialogInterface.BUTTON_NEGATIVE -> {

View File

@ -3,15 +3,12 @@ package com.tutpro.baresip
import android.app.Activity
import android.content.*
import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import android.widget.ImageButton
import android.widget.ListView
import java.io.File
class ContactsActivity : AppCompatActivity() {
internal lateinit var clAdapter: ContactListAdapter
@ -63,12 +60,10 @@ class ContactsActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
when (item.itemId) {
R.id.export_contacts -> {
if (saveContacts(dir, "contacts.bs"))
if (Contact.export())
Utils.alertView(this, "",
getString(R.string.exported_contacts))
else
@ -77,11 +72,11 @@ class ContactsActivity : AppCompatActivity() {
}
R.id.import_contacts -> {
if (restoreContacts(dir, "contacts.bs")) {
if (Contact.import()) {
Utils.alertView(this, "",
getString(R.string.imported_contacts))
clAdapter.notifyDataSetChanged()
saveContacts(applicationContext.filesDir, "contacts")
Contact.save()
} else
Utils.alertView(this,getString(R.string.error),
getString(R.string.import_error))
@ -111,33 +106,6 @@ class ContactsActivity : AppCompatActivity() {
companion object {
fun saveContacts(path: File, file: String): Boolean {
var contents = ""
for (c in Contact.contacts())
contents += "\"${c.name}\" <${c.uri}>\n"
return Utils.putFileContents(File(path, file), contents)
}
fun restoreContacts(path: File, file: String): Boolean {
val content = Utils.getFileContents(File(path, file))
if (content == "Failed") return false
Api.contacts_remove()
Contact.contacts().clear()
content.lines().forEach {
val parts = it.split("\"")
if (parts.size == 3) {
val name = parts[1]
var uri = parts[2].trim()
if (uri.startsWith("<"))
uri = uri.substringAfter("<").substringBefore(">")
// Currently no need to make baresip aware of the contact
// Api.contact_add("\"$name\" $uri")
Contact.contacts().add(Contact(name, uri))
}
}
return true
}
fun findContactURI(name: String): String {
for (c in Contact.contacts())
if (c.name == name)

View File

@ -404,8 +404,6 @@ class MainActivity : AppCompatActivity() {
}
}
if (intentAction == null)
CallHistory.restore(applicationContext.filesDir.absolutePath)
callsButton.setOnClickListener {
if (aorSpinner.selectedItemPosition >= 0) {
val i = Intent(this@MainActivity, CallsActivity::class.java)
@ -891,22 +889,28 @@ class MainActivity : AppCompatActivity() {
}
baresipService.setAction("ToggleSpeaker")
startService(baresipService)
return true
}
R.id.config -> {
i = Intent(this, ConfigActivity::class.java)
startActivityForResult(i, CONFIG_CODE)
return true
}
R.id.accounts -> {
i = Intent(this, AccountsActivity::class.java)
startActivityForResult(i, ACCOUNTS_CODE)
return true
}
R.id.export_all -> {
if (Utils.requestPermission(this,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
askPassword(getString(R.string.encrypt_password))
}
R.id.import_all -> {
if (Utils.requestPermission(this,
android.Manifest.permission.READ_EXTERNAL_STORAGE))
askPassword(getString(R.string.decrypt_password))
}
R.id.about -> {
i = Intent(this, AboutActivity::class.java)
startActivityForResult(i, ABOUT_CODE)
return true
}
R.id.restart, R.id.quit -> {
if (stopState == "initial") {
@ -921,10 +925,77 @@ class MainActivity : AppCompatActivity() {
System.exit(0)
}
}
return true
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun askPassword(title: String) {
val builder = android.app.AlertDialog.Builder(this)
builder.setTitle(title)
val viewInflated = LayoutInflater.from(this)
.inflate(R.layout.password_dialog, findViewById(android.R.id.content) as ViewGroup,
false)
val input = viewInflated.findViewById(R.id.password) as EditText
builder.setView(viewInflated)
builder.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
val password = input.text.toString()
if (password != "") {
if (title == getString(R.string.encrypt_password))
exportAll(password)
else
importAll(password)
}
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.cancel()
}
builder.show()
}
private fun exportAll(password: String) {
val files = arrayOf("accounts", "config", "contacts", "history", "messages", "uuid",
"zrtp_cache.dat", "zrtp_zid", "cert.pem", "ca_cert", "ca_certs.crt")
val exportFilePath = BaresipService.downloadsPath + "/baresip.bs"
val zipFilePath = BaresipService.filesPath + "/baresip.zip"
if (!Utils.zip(files, "baresip.zip")) {
Utils.alertView(this, getString(R.string.error), "Failed to write zip file 'baresip.zip")
return
}
val content = Utils.getFileContents(zipFilePath)
if (content == null) {
Utils.alertView(this, getString(R.string.error), "Failed to read zip file 'baresip.zip")
return
}
if (!Utils.encryptToFile(exportFilePath, content, password)) {
Utils.alertView(this, getString(R.string.error), getString(R.string.export_all_failed))
return
}
Utils.alertView(this, getString(R.string.info), getString(R.string.exported_all))
Utils.deleteFile(zipFilePath)
}
private fun importAll(password: String) {
val importFilePath = BaresipService.downloadsPath + "/baresip.bs"
val zipFilePath = BaresipService.filesPath + "/baresip.zip"
val zipData = Utils.decryptFromFile(importFilePath, password)
if (zipData == null) {
Utils.alertView(this, getString(R.string.error), getString(R.string.import_all_failed))
return
}
if (!Utils.putFileContents(zipFilePath, zipData)) {
Utils.alertView(this, getString(R.string.error),
"Failed to write file 'baresip.zip'")
return
}
if (!Utils.unZip(zipFilePath)) {
Utils.alertView(this, getString(R.string.error),
"Failed to unzip file 'baresip.zip'")
return
}
Utils.alertView(this, getString(R.string.info), getString(R.string.imported_all))
Utils.deleteFile(zipFilePath)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View File

@ -36,7 +36,7 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
while (it.hasNext()) if (it.next().aor == aor) it.remove()
}
fun saveMessages() {
fun save() {
val file = File(BaresipService.filesPath, "messages")
try {
val fos = FileOutputStream(file)
@ -51,7 +51,7 @@ class Message(val aor: String, val peerUri: String, val message: String, val tim
}
}
fun restoreMessages() {
fun restore() {
val file = File(BaresipService.filesPath, "messages")
if (file.exists()) {
try {

View File

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import java.io.File
import java.nio.charset.StandardCharsets
class RunOnStartup : BroadcastReceiver() {
@ -13,8 +13,8 @@ class RunOnStartup : BroadcastReceiver() {
Log.i("Baresip", "RunOnStartup received intent ${intent.action}")
if ((intent.action == Intent.ACTION_BOOT_COMPLETED) or
(intent.action == "com.tutpro.baresip.Restart")) {
val configFile = File(context.filesDir.absolutePath + "/config")
val config = Utils.getFileContents(configFile)
val configPath = context.filesDir.absolutePath + "/config"
val config = String(Utils.getFileContents(configPath)!!, StandardCharsets.ISO_8859_1)
val asCv = Utils.getNameValue(config,"auto_start")
if ((asCv.size > 0) && (asCv[0] == "yes")) {
Log.d("Baresip", "Start baresip upon boot completed or restart")

View File

@ -4,8 +4,6 @@ import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.support.v7.app.AlertDialog
import android.os.PowerManager
import android.app.KeyguardManager
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
@ -13,8 +11,6 @@ import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.text.Editable
import android.text.TextWatcher
import android.util.Base64
import android.util.Base64.encodeToString
import kotlin.collections.ArrayList
import kotlin.Exception
@ -22,57 +18,16 @@ import kotlin.Exception
import java.io.*
import java.security.SecureRandom
import java.util.*
import java.nio.ByteBuffer
import java.util.zip.*
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
object Utils {
fun getFileContents(file: File): String {
if (!file.exists()) {
Log.e("Baresip", "Failed to find file: " + file.path)
return "Failed"
} else {
val length = file.length().toInt()
val bytes = ByteArray(length)
try {
val `in` = FileInputStream(file)
try {
`in`.read(bytes)
} finally {
`in`.close()
}
return String(bytes)
} catch (e: java.io.IOException) {
Log.e("Baresip", "Failed to read file: " + file.path + ": " +
e.toString())
return "Failed"
}
}
}
fun putFileContents(file: File, contents: String): Boolean {
try {
val fOut = FileOutputStream(file.absoluteFile, false)
val fWriter = OutputStreamWriter(fOut)
try {
fWriter.write(contents)
fWriter.close()
fOut.close()
} catch (e: java.io.IOException) {
Log.e("Baresip", "Failed to put contents to file: " + e.toString())
return false
}
} catch (e: java.io.FileNotFoundException) {
Log.e("Baresip", "Failed to find contents file: " + e.toString())
return false
}
return true
}
fun getNameValue(string: String, name: String): ArrayList<String> {
val lines = string.split("\n")
val result = ArrayList<String>()
@ -90,24 +45,6 @@ object Utils {
return result
}
fun copyAssetToFile(context: Context, asset: String, path: String) {
try {
val `is` = context.assets.open(asset)
val os = FileOutputStream(path)
val buffer = ByteArray(512)
var byteRead: Int = `is`.read(buffer)
while (byteRead != -1) {
os.write(buffer, 0, byteRead)
byteRead = `is`.read(buffer)
}
os.close()
`is`.close()
} catch (e: IOException) {
Log.e("Baresip", "Failed to read asset " + asset + ": " +
e.toString())
}
}
fun alertView(context: Context, title: String, message: String) {
val builder = AlertDialog.Builder(context)
builder.setTitle(title)
@ -142,7 +79,7 @@ object Utils {
}
}
fun aorUser(aor: String): String {
private fun aorUser(aor: String): String {
val user = aor.substringBefore("@")
return if (user == aor) "" else user
}
@ -164,20 +101,20 @@ object Utils {
return Regex("^(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))$").matches(ip)
}
fun checkIpV6(ip: String): Boolean {
private fun checkIpV6(ip: String): Boolean {
return Regex("^(([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4})$").matches(ip)
}
fun checkIpv6InBrackets(bracketedIp: String): Boolean {
private fun checkIpv6InBrackets(bracketedIp: String): Boolean {
return bracketedIp.startsWith("[") && bracketedIp.endsWith("]") &&
checkIpV6(bracketedIp.substring(1, bracketedIp.length - 2))
}
fun checkIp(ip: String): Boolean {
private fun checkIp(ip: String): Boolean {
return checkIpV4(ip) || checkIpV6(ip)
}
fun checkUriUser(user: String): Boolean {
private fun checkUriUser(user: String): Boolean {
for (c in user)
if (!(c.isLetterOrDigit() || c in "-_.!~*'()&=+$,;?/")) return false
return user.length > 0
@ -193,7 +130,7 @@ object Utils {
return true
}
fun checkPort(port: String): Boolean {
private fun checkPort(port: String): Boolean {
val number = port.toIntOrNull()
if (number == null) return false
return (number > 0) && (number < 65536)
@ -208,7 +145,7 @@ object Utils {
checkPort(ipPort.substringAfterLast(":"))
}
fun checkDomainPort(domainPort: String): Boolean {
private fun checkDomainPort(domainPort: String): Boolean {
return checkDomain(domainPort.substringBeforeLast(":")) &&
checkPort(domainPort.substringAfterLast(":"))
}
@ -218,13 +155,13 @@ object Utils {
checkIpPort(hostPort) || checkDomainPort(hostPort)
}
fun checkParams(params: String): Boolean {
private fun checkParams(params: String): Boolean {
for (param in params.split(";"))
if (!checkParam(param)) return false
return true
}
fun checkParam(param: String): Boolean {
private fun checkParam(param: String): Boolean {
val nameValue = param.split("=")
if (nameValue.size == 1)
/* Todo: do proper check */
@ -295,23 +232,6 @@ object Utils {
return appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
}
fun isDeviceLocked(context: Context): Boolean {
val isLocked: Boolean
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
val inKeyguardRestrictedInputMode = keyguardManager.inKeyguardRestrictedInputMode()
if (inKeyguardRestrictedInputMode) {
isLocked = true
} else {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
isLocked = !powerManager.isInteractive
}
Log.d("Baresip", "Now device is ${if (isLocked) "locked" else "unlocked"}")
return isLocked
}
fun dtmfWatcher(callp: String): TextWatcher {
return object : TextWatcher {
override fun beforeTextChanged(sequence: CharSequence, start: Int, count: Int, after: Int) {}
@ -341,81 +261,183 @@ object Utils {
return true
}
fun encrypt(plainText: String, password: String): String {
val sr = SecureRandom.getInstance("SHA1PRNG")
val salt = ByteArray(16)
sr.nextBytes(salt)
val iterationCount = Random().nextInt(1024) + 512
val spec = PBEKeySpec(password.toCharArray(), salt, iterationCount, 256)
val secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(spec)
val iv = ByteArray(12)
SecureRandom().nextBytes(iv)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val parameterSpec = GCMParameterSpec(128, iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec)
val cipherText = cipher.doFinal(plainText.toByteArray())
val byteBuffer = ByteBuffer.allocate(Int.SIZE_BYTES + salt.size + Int.SIZE_BYTES +
Int.SIZE_BYTES + iv.size + cipherText.size)
byteBuffer.putInt(salt.size)
byteBuffer.put(salt)
byteBuffer.putInt(iterationCount)
byteBuffer.putInt(iv.size)
byteBuffer.put(iv)
byteBuffer.put(cipherText)
return encodeToString(byteBuffer.array(), Base64.DEFAULT)
fun copyAssetToFile(context: Context, asset: String, path: String) {
try {
val `is` = context.assets.open(asset)
val os = FileOutputStream(path)
val buffer = ByteArray(512)
var byteRead: Int = `is`.read(buffer)
while (byteRead != -1) {
os.write(buffer, 0, byteRead)
byteRead = `is`.read(buffer)
}
os.close()
`is`.close()
} catch (e: IOException) {
Log.e("Baresip", "Failed to copy asset '$asset' to file: $e")
}
}
fun decrypt(cipherMessage: String, password: String): String {
val byteBuffer = ByteBuffer.wrap(Base64.decode(cipherMessage, Base64.DEFAULT))
val saltLength = byteBuffer.getInt()
if (saltLength != 16) {
Log.w("Baresip", "invalid salt length $saltLength")
return ""
}
val salt = ByteArray(saltLength)
byteBuffer.get(salt)
val iterationCount = byteBuffer.getInt()
if ((iterationCount < 512) || (iterationCount > 1023 + 512)) {
Log.w("Baresip", "invalid iteratorCount $iterationCount")
return ""
}
val spec = PBEKeySpec(password.toCharArray(), salt, iterationCount, 256)
val secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(spec)
val ivLength = byteBuffer.getInt()
if (ivLength != 12) {
Log.d("Baresip", "invalid iv length $ivLength")
return ""
}
val iv = ByteArray(ivLength)
byteBuffer.get(iv)
val cipherText = ByteArray(byteBuffer.remaining())
byteBuffer.get(cipherText)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv))
fun deleteFile(filePath: String) {
val file = File(filePath)
if (file.exists()) file.delete()
}
fun getFileContents(filePath: String): ByteArray? {
try {
return String(cipher.doFinal(cipherText))
return File(filePath).readBytes()
} catch(e: FileNotFoundException) {
Log.e("Baresip", "File '$filePath' not found: ${e.printStackTrace()}")
return null
} catch (e: Exception) {
Log.w("Baresip", "Decryption failed ${e.printStackTrace()}")
return ""
Log.e("Baresip", "Failed to read file '$filePath': ${e.printStackTrace()}")
return null
}
}
fun putFileContents(filePath: String, contents: ByteArray): Boolean {
try {
File(filePath).writeBytes(contents)
}
catch (e: IOException) {
Log.e("Baresip", "Failed to write file '$filePath': $e")
return false
}
return true
}
class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray):
Serializable {
val serialVersionUID = -29238082928391L
}
private fun encrypt(content: ByteArray, password: CharArray): Crypto? {
var obj: Crypto? = null
try {
val sr = SecureRandom()
val salt = ByteArray(128)
sr.nextBytes(salt)
val iterationCount = Random().nextInt(1024) + 512
val pbKeySpec = PBEKeySpec(password, salt, iterationCount, 128)
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
val keySpec = SecretKeySpec(keyBytes, "AES")
val ivRandom = SecureRandom()
val iv = ByteArray(16)
ivRandom.nextBytes(iv)
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
val cipherData = cipher.doFinal(content)
obj = Crypto(salt, iterationCount, iv, cipherData)
} catch (e: Exception) {
Log.e("Baresip", "Encrypt failed: ${e.printStackTrace()}")
}
return obj
}
private fun decrypt(obj: Crypto, password: CharArray): ByteArray? {
var plainData: ByteArray? = null
try {
val pbKeySpec = PBEKeySpec(password, obj.salt, obj.iter, 128)
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
val keySpec = SecretKeySpec(keyBytes, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
val ivSpec = IvParameterSpec(obj.iv)
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
plainData = cipher.doFinal(obj.data)
} catch (e: Exception) {
Log.e("Baresip", "Decrypt failed: ${e.printStackTrace()}")
}
return plainData
}
fun encryptToFile(filePath: String, content: ByteArray, password: String): Boolean {
val obj = encrypt(content, password.toCharArray())
try {
ObjectOutputStream(FileOutputStream(File(filePath))).use {
it.writeObject(obj)
}
} catch (e: Exception) {
Log.e("Baresip", "Write failed: ${e.printStackTrace()}")
return false
}
return true
}
fun decryptFromFile(filePath: String, password: String): ByteArray? {
var plainData: ByteArray? = null
try {
ObjectInputStream(FileInputStream(File(filePath))).use { it ->
val obj = it.readObject() as Crypto
plainData = decrypt(obj, password.toCharArray())
}
} catch (e: Exception) {
Log.e("Baresip", "Decrypt failed from file '$filePath'")
}
return plainData
}
fun zip(fileNames: Array<String>, zipFileName: String): Boolean {
val zipFilePath = BaresipService.filesPath + "/" + zipFileName
try {
ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFilePath))).use { out ->
val data = ByteArray(1024)
for (file in fileNames) {
val filePath = BaresipService.filesPath + "/" + file
if (File(filePath).exists()) {
FileInputStream(filePath).use { fi ->
BufferedInputStream(fi).use { origin ->
val entry = ZipEntry(filePath)
out.putNextEntry(entry)
while (true) {
val readBytes = origin.read(data)
if (readBytes == -1) break
out.write(data, 0, readBytes)
}
}
}
}
}
}
} catch (e: IOException) {
Log.e("Baresip", "Failed to zip file '$zipFilePath': $e")
return false
}
return true
}
fun unZip(zipFilePath: String): Boolean {
try {
ZipFile(zipFilePath).use { zip ->
zip.entries().asSequence().forEach { entry ->
zip.getInputStream(entry).use { input ->
File(entry.name).outputStream().use { output ->
input.copyTo(output)
}
}
}
}
} catch (e: IOException) {
Log.e("Baresip", "Failed to unzip file '$zipFilePath': $e")
return false
}
return true
}
fun dumpIntent(intent: Intent) {
val bundle: Bundle = intent.extras ?: return
val keys = bundle.keySet()
val it = keys.iterator()
Log.d("Baresip", "Dumping intent start")
while (it.hasNext()) {
val key = it.next()
Log.d("Baresip","[" + key + "=" + bundle.get(key)+"]");
}
Log.d("Baresip", "Dumping intent finish")
}
}

View File

@ -6,6 +6,12 @@
<item android:id="@+id/accounts"
android:title="@string/accounts" />
<item android:id="@+id/export_all"
android:title="@string/export_all" />
<item android:id="@+id/import_all"
android:title="@string/import_all" />
<item android:id="@+id/about"
android:title="@string/about" />

View File

@ -238,26 +238,20 @@
<string name="contact_action_question">Haluatko soittaa tai lähettää
viestin kontaktille \'%1$s\'?</string>
<string name="send_message">Lähetä viesti</string>
<string name="contact_delete_question">Haluatko poistaa kontaktin
\'%1$s\'?
</string>
<string name="contacts_exceeded">Kontaktiesi enimmäismäärä %1$d on
ylittynyt.
</string>
<string name="contact_delete_question">Haluatko poistaa kontaktin \'%1$s\'?</string>
<string name="contacts_exceeded">Kontaktiesi enimmäismäärä %1$d on ylittynyt.</string>
<string name="export_contacts">Tallenna</string>
<string name="exported_contacts">Kontaktit tallennettiin
Download-kansion tiedostoon \'contacts.bs\'.
<string name="exported_contacts">Kontaktit tallennettiin Download-kansion tiedostoon
\'contacts.bs\'.
</string>
<string name="export_error">"Kontaktien tallennus Download kansioon epäonnistui.
Tarkista Sovellukset → baresip → Käyttöluvat → Tallennustila.
</string>
<string name="import_contacts">Palauta</string>
<string name="imported_contacts">Kontaktit palautettiin Download-kansiosta.
</string>
<string name="import_error">Kontantien palauttaminen
Download-kansiosta epäonnistui.
<string name="imported_contacts">Kontaktit palautettiin.</string>
<string name="import_error">Kontantien palauttaminen Download-kansiosta epäonnistui.
Tarkista Sovellukset → baresip → Käyttöluvat → Tallennustila ja että tiedosto \'contacts.bs\' on
kansiossa.
Download-kansiossa.
</string>
<!-- Generic -->
<string name="alert">Varoitus</string>
@ -311,14 +305,12 @@
TLS sertifikaattitiedostosta. Niiden oletusarvot on palautettu. Käynnistä
baresip uudelleen.
</string>
<string name="registering_failed">Tilin \'%1$s\' rekisteröinti
epäonnistui.</string>
<string name="registering_failed">Tilin \'%1$s\' rekisteröinti epäonnistui.</string>
<string name="no_microphone_permission">Et ole sallinut mikrofonin
käyttöä.
</string>
<string name="verify">Todenna</string>
<string name="verify_sas">Todennatko SAS:n &lt;%1$s&gt;
&lt;%2$s&gt;?
<string name="verify_sas">Todennatko SAS:n &lt;%1$s&gt; &lt;%2$s&gt;?
</string>
<string name="transfer_query">Hyväksytkö puhelun siirron kohteeseen
\'%1$s\'?
@ -334,8 +326,23 @@
<string name="call_is_secure">Tämä pulelu on turvallinen ja kohde on
todennettu! Haluatko poistaa todennuksen?</string>
<string name="unverify">Poista todennus</string>
qqqq <string name="tls_ca_file">TLS CA-tiedosto</string>
<string name="tls_ca_file_help">Jos merkitty, tiedosto \'ca_certs.crt\', joka sisältää todennusauktoriteettien TLS-sertifikaatit, on ladattu tai ladataan Download-kansiosta.</string>
<string name="tls_ca_file">TLS CA-tiedosto</string>
<string name="tls_ca_file_help">Jos merkitty, tiedosto \'ca_certs.crt\', joka sisältää
todennusauktoriteettien TLS-sertifikaatit, on ladattu tai ladataan Download-kansiosta.
</string>
<string name="tls_certificate_file">TLS sertifikaattitiedosto</string>
<string name="tls_certificate_file_help">"Jos merkitty, tiedosto \'cert.pem\', joka sisältää tämän baresip-sovelluksen TLS-sertifikaatin ja yksityisen avaimen, on ladattu tai ladataan Download-kansiosta."</string>
<string name="tls_certificate_file_help">"Jos merkitty, tiedosto \'cert.pem\', joka sisältää
tämän baresip-sovelluksen TLS-sertifikaatin ja yksityisen avaimen, on ladattu tai ladataan
Download-kansiosta."
</string>
<string name="exported_all">Sovelluksen kaikki data talletettiin Download-kansion tiedostoon
\'baresip.bs\'.
</string>
<string name="imported_all">Sovelluksen kaikki data palautettiin. Sinun pitää käynnistää
baresip uudelleen.
</string>
<string name="import_all_failed">Sovelluksen data palauttaminen Download-kansiosta epäonnistui.
Tarkista Sovellukset → baresip → Käyttöluvat → Tallennustila ja että tallennettu tiedosto
\'baresip.bs\' on kansiossa ja (jos on) että annoit oikean salasanan.
</string>
</resources>

View File

@ -103,10 +103,9 @@
Check Apps → baresip → Permissions → Storage."
</string>
<string name="import_accounts">Import</string>
<string name="imported_accounts">Imported accounts from Download folder. You need to restart
baresip.</string>
<string name="imported_accounts">Accounts imported. You need to restart baresip.</string>
<string name="import_accounts_error">Failed to import accounts from Download folder file.
Check Apps → baresip → Permissions → Storage and that file \'accounts.bs\' exists in
Check Apps → baresip → Permissions → Storage and that exported file \'accounts.bs\' exists in
the folder and, if so, you gave correct Decrypt Password."
</string>
<string name="delete_account">Do you want to delete account \'%1$s\'?</string>
@ -195,7 +194,8 @@ Check Apps → baresip → Permissions → Storage and that file \'accounts.bs\'
Valid values are 6000-510000. Factory default is 28000.</string>
<string name="_28000" translatable="false">28000</string>
<string name="opus_packet_loss">Expected Opus packet-loss</string>
<string name="opus_packet_loss_help">Expected Opus audio stream packet loss percentage, from 0-100. By default 0, turning off Opus Forward Error Correction (FEC).</string>
<string name="opus_packet_loss_help">Expected Opus audio stream packet loss percentage,
from 0100. By default 0, turning off Opus Forward Error Correction (FEC).</string>
<string name="_0" translatable="false">0</string>
<string name="invalid_opus_bitrate">Invalid Opus bitrate</string>
<string name="invalid_opus_packet_loss">Invalid Opus Packet Loss Percentage</string>
@ -230,7 +230,7 @@ Check Apps → baresip → Permissions → Storage and that file \'accounts.bs\'
<string name="exported_contacts">Exported contacts to Download folder file \'contacts.bs\'.</string>
<string name="export_error">"Failed to export contacts to Download folder.
Check Apps → baresip → Permissions → Storage."</string>
<string name="imported_contacts">Imported contacts from Download folder.</string>
<string name="imported_contacts">Contacs imported.</string>
<string name="import_error">Failed to import contacts from Download folder. Check Apps →
baresip → Permissions → Storage and that file \'contacts.bs\' exists in the folder.</string>
@ -256,6 +256,8 @@ Check Apps → baresip → Permissions → Storage and that file \'accounts.bs\'
<string name="dots" translatable="false"></string>
<!-- Main Activity -->
<string name="export_all">Export</string>
<string name="import_all">Import</string>
<string name="about">About</string>
<string name="restart">Restart</string>
<string name="quit">Quit</string>
@ -285,7 +287,8 @@ Check Apps → baresip → Permissions → Storage and that file \'accounts.bs\'
<string name="dialpad">Dialpad</string>
<string name="call_already_active">You already have an active call.</string>
<string name="start_failed">Baresip failed to start. This may be due to invalid Listen Address
or TLS file. They have been reset. Restart baresip.</string>
or TLS file. They have been reset. Restart baresip.
</string>
<string name="registering_failed">Registering of \`%1$s\` failed.</string>
<string name="no_microphone_permission">You have not granted microphone permission.</string>
<string name="verify">Verify</string>
@ -297,7 +300,16 @@ Check Apps → baresip → Permissions → Storage and that file \'accounts.bs\'
<string name="call_not_secure">This call is NOT secure!</string>
<string name="peer_not_verified">This call is SECURE, but peer is NOT verified!</string>
<string name="call_is_secure">This call is SECURE and peer is VERIFIED!
Do you want to unverify the peer?</string>
Do you want to unverify the peer?
</string>
<string name="unverify">Unverify</string>
<string name="exported_all">Exported all application data to Download folder file \'baresip.bs\'.</string>
<string name="export_all_failed">Failed to export application data to Download folder file
\'baresip.bs\'. Check Apps → baresip → Permissions → Storage</string>
<string name="imported_all">All application data imported. You need to restart baresip.</string>
<string name="import_all_failed">Failed to import application data from Download folder. Check Apps →
baresip → Permissions → Storage and that exported file \'baresip.bs\' exists in the folder
and, if so, you gave correct Decrypt Password.
</string>
</resources>