diff --git a/app/build.gradle b/app/build.gradle index 4daab8d8..f93290f1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { applicationId = 'com.tutpro.baresip' minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode = 163 versionName = '29.2.1' externalNativeBuild { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 42848c7d..d03cf471 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,8 @@ - + diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index 4d0c061e..01578e35 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -1103,18 +1103,17 @@ Java_com_tutpro_baresip_Api_account_1mediaaf(JNIEnv *env, jobject thiz, jstring return account_mediaaf(acc); } -JNIEXPORT void JNICALL +JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1mediaaf(JNIEnv *env, jobject thiz, jstring jAcc, jint jAf) { const char *native_acc = (*env)->GetStringUTFChars(env, jAcc, 0); struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); (*env)->ReleaseStringUTFChars(env, jAcc, native_acc); LOGD("setting account mediaaf to '%d'\n", jAf); - account_set_mediaaf(acc, jAf); + return account_set_mediaaf(acc, jAf); } JNIEXPORT jstring JNICALL -Java_com_tutpro_baresip_Api_account_1extra(JNIEnv *env, jobject thiz, jstring javaAcc) -{ +Java_com_tutpro_baresip_Api_account_1extra(JNIEnv *env, jobject thiz, jstring javaAcc) { const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); struct account *acc = (struct account *) strtoul(native_acc, NULL, 10); (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); diff --git a/app/src/main/kotlin/com/tutpro/baresip/Api.kt b/app/src/main/kotlin/com/tutpro/baresip/Api.kt index cc31edc0..f6d5247f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Api.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Api.kt @@ -45,7 +45,7 @@ object Api { external fun account_dtmfmode(acc: String): Int external fun account_set_dtmfmode(acc: String, mode: Int): Int external fun account_mediaaf(acc: String): Int - external fun account_set_mediaaf(acc: String, af: Int) + external fun account_set_mediaaf(acc: String, af: Int): Int external fun account_extra(acc: String): String external fun account_debug(acc: String) diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 481a5ec9..856fa513 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -415,7 +415,7 @@ class BaresipService: Service() { Log.d(TAG, "uaAdd ${ua.account.aor} at BaresipService") uas.add(ua) if (ua.account.preferIPv6Media) - Api.ua_set_media_af(ua.uap, Api.AF_INET6) + Api.account_set_mediaaf(ua.account.accp, Api.AF_INET6) if (Api.ua_isregistered(uap)) { Log.d(TAG, "Ua ${ua.account.aor} is registered") status.add(R.drawable.dot_green) diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index e17b770d..690b4521 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -14,6 +14,8 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.CountDownTimer +import android.provider.DocumentsContract +import android.provider.MediaStore import android.text.InputType import android.text.TextWatcher import android.text.method.HideReturnsTransformationMethod @@ -21,6 +23,7 @@ import android.text.method.PasswordTransformationMethod import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.* +import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate @@ -28,6 +31,8 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.tutpro.baresip.databinding.ActivityMainBinding import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream import java.net.URLDecoder class MainActivity : AppCompatActivity() { @@ -62,6 +67,8 @@ class MainActivity : AppCompatActivity() { private lateinit var stopState: String private var speakerIcon: MenuItem? = null private lateinit var swipeRefresh: SwipeRefreshLayout + private var downloadsInputStream: FileInputStream? = null + private var downloadsOutputStream: FileOutputStream? = null private lateinit var baresipService: Intent @@ -1074,15 +1081,23 @@ class MainActivity : AppCompatActivity() { } R.id.backup -> { - if (Utils.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, - BACKUP_PERMISSION_REQUEST_CODE)) - askPassword(getString(R.string.encrypt_password)) + if (Build.VERSION.SDK_INT >= 29) { + pickupFileFromDownloads(BACKUP_CODE) + } else { + if (Utils.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, + BACKUP_PERMISSION_REQUEST_CODE)) { + val path = BaresipService.downloadsPath + "/baresip.bs" + downloadsOutputStream = FileOutputStream(File(path)) + askPassword(getString(R.string.encrypt_password)) + } + } } R.id.restore -> { - if (Utils.requestPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE, + if (Build.VERSION.SDK_INT >= 29 || + Utils.requestPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE, RESTORE_PERMISSION_REQUEST_CODE)) - askPassword(getString(R.string.decrypt_password)) + startRestore() } R.id.about -> { @@ -1163,6 +1178,27 @@ class MainActivity : AppCompatActivity() { updateIcons(Account.ofAor(activityAor)!!) } + BACKUP_CODE -> { + if (resultCode == Activity.RESULT_OK) + data?.data?.also { + Log.d(TAG, "******** $it") + downloadsOutputStream = applicationContext.contentResolver.openOutputStream(it) + as FileOutputStream + if (downloadsOutputStream != null) + askPassword(getString(R.string.encrypt_password)) + } + } + + RESTORE_CODE -> { + if (resultCode == Activity.RESULT_OK) + data?.data?.also { + downloadsInputStream = applicationContext.contentResolver.openInputStream(it) + as FileInputStream + if (downloadsInputStream != null) + askPassword(getString(R.string.decrypt_password)) + } + } + ABOUT_CODE -> { } @@ -1176,7 +1212,7 @@ class MainActivity : AppCompatActivity() { when (requestCode) { RECORD_PERMISSION_REQUEST_CODE -> { - if ((grantResults.size > 0) && (grantResults[0] != PackageManager.PERMISSION_GRANTED)) + if ((grantResults.isNotEmpty()) && (grantResults[0] != PackageManager.PERMISSION_GRANTED)) Utils.alertView(this, getString(R.string.notice), getString(R.string.no_calls), ::startBaresip) else @@ -1184,16 +1220,49 @@ class MainActivity : AppCompatActivity() { } BACKUP_PERMISSION_REQUEST_CODE -> - if ((grantResults.size > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) + if ((grantResults.isNotEmpty()) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) askPassword(getString(R.string.encrypt_password)) RESTORE_PERMISSION_REQUEST_CODE -> - if ((grantResults.size > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) - askPassword(getString(R.string.decrypt_password)) + if ((grantResults.isNotEmpty()) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) + startRestore() } } + private fun startRestore() { + if (Build.VERSION.SDK_INT >= 29) { + pickupFileFromDownloads(RESTORE_CODE) + } else { + val path = BaresipService.downloadsPath + "/baresip.bs" + downloadsInputStream = FileInputStream(File(path)) + askPassword(getString(R.string.decrypt_password)) + } + } + + @RequiresApi(29) + private fun pickupFileFromDownloads(activityCode: Int) { + val intent = when (activityCode) { + BACKUP_CODE -> { + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/octet-stream" + putExtra(Intent.EXTRA_TITLE, "baresip.bs") + putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) + } + } + RESTORE_CODE -> { + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/octet-stream" + putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) + } + } + else -> null + } + startActivityForResult(intent, activityCode) + } + private fun quitRestart(reStart: Boolean) { if (stopState == "initial") { Log.d(TAG, "quitRestart Restart = $reStart") @@ -1378,54 +1447,57 @@ class MainActivity : AppCompatActivity() { File(BaresipService.filesPath).walk().forEach { if (it.name.endsWith(".png")) files.add(it.name) } - val bsFile = getString(R.string.app_name) + ".bs" - val backupFilePath = BaresipService.downloadsPath + "/$bsFile" - val zipFile = getString(R.string.app_name) + ".zip" + val zipFile = getString(R.string.app_name_plus) + ".zip" val zipFilePath = BaresipService.filesPath + "/$zipFile" if (!Utils.zip(files, zipFile)) { Log.w(TAG, "Failed to write zip file '$zipFile'") Utils.alertView(this, getString(R.string.error), - String.format(getString(R.string.backup_failed), bsFile)) + String.format(getString(R.string.backup_failed), + downloadsOutputFile)) return } val content = Utils.getFileContents(zipFilePath) if (content == null) { Log.w(TAG, "Failed to read zip file '$zipFile'") Utils.alertView(this, getString(R.string.error), - String.format(getString(R.string.backup_failed), bsFile)) + String.format(getString(R.string.backup_failed), + downloadsOutputFile)) return } - if (!Utils.encryptToFile(backupFilePath, content, password)) { + if (!Utils.encryptToStream(downloadsOutputStream, content, password)) { Utils.alertView(this, getString(R.string.error), - String.format(getString(R.string.backup_failed), bsFile)) + String.format(getString(R.string.backup_failed), + downloadsOutputFile)) return } Utils.alertView(this, getString(R.string.info), - String.format(getString(R.string.backed_up), bsFile)) + String.format(getString(R.string.backed_up), + downloadsOutputFile)) Utils.deleteFile(File(zipFilePath)) } private fun restore(password: String) { - val bsFile = getString(R.string.app_name) + ".bs" - val backupFilePath = BaresipService.downloadsPath + "/$bsFile" - val zipFile = getString(R.string.app_name) + ".zip" + val zipFile = getString(R.string.app_name_plus) + ".zip" val zipFilePath = BaresipService.filesPath + "/$zipFile" - val zipData = Utils.decryptFromFile(backupFilePath, password) + val zipData = Utils.decryptFromStream(downloadsInputStream, password) if (zipData == null) { Utils.alertView(this, getString(R.string.error), - String.format(getString(R.string.restore_failed), bsFile)) + String.format(getString(R.string.restore_failed), + downloadsInputFile)) return } if (!Utils.putFileContents(zipFilePath, zipData)) { Log.w(TAG, "Failed to write zip file '$zipFile'") Utils.alertView(this, getString(R.string.error), - String.format(getString(R.string.restore_failed), bsFile)) + String.format(getString(R.string.restore_failed), + downloadsInputFile)) return } if (!Utils.unZip(zipFilePath)) { Log.w(TAG, "Failed to unzip file '$zipFile'") Utils.alertView(this, getString(R.string.error), - String.format(getString(R.string.restore_failed), bsFile)) + String.format(getString(R.string.restore_failed), + downloadsInputFile)) return } Utils.deleteFile(File(zipFilePath)) @@ -1797,6 +1869,8 @@ class MainActivity : AppCompatActivity() { const val CONTACT_CODE = 7 const val CHATS_CODE = 8 const val CHAT_CODE = 9 + const val BACKUP_CODE = 10 + const val RESTORE_CODE = 11 const val BACKUP_PERMISSION_REQUEST_CODE = 1 const val RESTORE_PERMISSION_REQUEST_CODE = 2 diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index f9235e17..11307276 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -11,7 +11,9 @@ import android.graphics.Bitmap.createScaledBitmap import android.graphics.Color import android.net.LinkAddress import android.net.LinkProperties +import android.net.Uri import android.os.Bundle +import android.provider.OpenableColumns import android.text.Editable import android.text.TextWatcher import android.view.View @@ -418,14 +420,14 @@ object Utils { } fun getFileContents(filePath: String): ByteArray? { - try { - return File(filePath).readBytes() + return try { + File(filePath).readBytes() } catch (e: FileNotFoundException) { Log.e("Baresip", "File '$filePath' not found: ${e.printStackTrace()}") - return null + null } catch (e: Exception) { Log.e("Baresip", "Failed to read file '$filePath': ${e.printStackTrace()}") - return null + null } } @@ -440,6 +442,18 @@ object Utils { return true } + fun fileNameOfUri(ctx: Context, uri: Uri): String { + val cursor = ctx.contentResolver.query(uri, null, null, null, null) + var name = "" + if (cursor != null) { + val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + name = cursor.getString(index) + cursor.close() + } + return name + } + fun saveBitmap(bitmap: Bitmap, file: File): Boolean { if (file.exists()) file.delete() try { @@ -504,29 +518,28 @@ object Utils { return plainData } - - fun encryptToFile(filePath: String, content: ByteArray, password: String): Boolean { + fun encryptToStream(stream: FileOutputStream?, content: ByteArray, password: String): Boolean { val obj = encrypt(content, password.toCharArray()) try { - ObjectOutputStream(FileOutputStream(File(filePath))).use { + ObjectOutputStream(stream).use { it.writeObject(obj) } } catch (e: Exception) { - Log.e("Baresip", "Write failed: ${e.printStackTrace()}") + Log.w("Baresip", "encryptToStream failed: $e") return false } return true } - fun decryptFromFile(filePath: String, password: String): ByteArray? { + fun decryptFromStream(stream: FileInputStream?, password: String): ByteArray? { var plainData: ByteArray? = null try { - ObjectInputStream(FileInputStream(File(filePath))).use { + ObjectInputStream(stream).use { val obj = it.readObject() as Crypto plainData = decrypt(obj, password.toCharArray()) } } catch (e: Exception) { - Log.e("Baresip", "Decrypt failed from file '$filePath'") + Log.w("Baresip", "decryptFromStream failed: $e") } return plainData }