DownloadsFileSaver: encapsulate permission checks

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2020-09-16 17:56:07 +02:00
parent eebeece856
commit a9ec828506
6 changed files with 70 additions and 91 deletions

View File

@ -1,5 +1,6 @@
buildscript {
ext {
activityVersion = '1.2.0-alpha08'
agpVersion = '4.0.1'
annotationsVersion = '1.1.0'
appcompatVersion = '1.2.0'
@ -11,7 +12,7 @@ buildscript {
coreKtxVersion = '1.3.1'
coroutinesVersion = '1.3.9'
desugarVersion = '1.0.10'
fragmentVersion = '1.2.5'
fragmentVersion = '1.3.0-alpha08'
jsr305Version = '3.0.2'
junitVersion = '4.13'
kotlinVersion = '1.4.10'

View File

@ -57,6 +57,7 @@ android {
dependencies {
implementation project(":tunnel")
implementation "androidx.activity:activity-ktx:$activityVersion"
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"

View File

@ -60,7 +60,6 @@ import java.util.regex.Matcher
import java.util.regex.Pattern
class LogViewerActivity : AppCompatActivity() {
private lateinit var binding: LogViewerActivityBinding
private lateinit var logAdapter: LogEntryAdapter
private var logLines = arrayListOf<LogLine>()
@ -161,15 +160,15 @@ class LogViewerActivity : AppCompatActivity() {
withContext(Dispatchers.IO) {
try {
outputFile = DownloadsFileSaver.save(this@LogViewerActivity, "wireguard-log.txt", "text/plain", true)
outputFile?.outputStream.use {
it?.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
}
outputFile?.outputStream?.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
} catch (e: Throwable) {
outputFile?.delete()
exception = e
}
}
saveButton?.isEnabled = true
if (outputFile == null)
return
Snackbar.make(findViewById(android.R.id.content),
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
else getString(R.string.log_export_error, ErrorMessages[exception]),

View File

@ -5,13 +5,9 @@
package com.wireguard.android.activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.SparseArray
import android.view.MenuItem
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@ -23,35 +19,11 @@ import com.wireguard.android.util.ModuleLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.ArrayList
import java.util.Arrays
/**
* Interface for changing application-global persistent settings.
*/
class SettingsActivity : ThemeChangeAwareActivity() {
private val permissionRequestCallbacks = SparseArray<(permissions: Array<String>, granted: IntArray) -> Unit>()
private var permissionRequestCounter = 0
fun ensurePermissions(permissions: Array<String>, cb: (permissions: Array<String>, granted: IntArray) -> Unit) {
val needPermissions: MutableList<String> = ArrayList(permissions.size)
permissions.forEach {
if (ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED) {
needPermissions.add(it)
}
}
if (needPermissions.isEmpty()) {
val granted = IntArray(permissions.size)
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED)
cb.invoke(permissions, granted)
return
}
val idx = permissionRequestCounter++
permissionRequestCallbacks.put(idx, cb)
ActivityCompat.requestPermissions(this,
needPermissions.toTypedArray(), idx)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
@ -69,16 +41,6 @@ class SettingsActivity : ThemeChangeAwareActivity() {
return super.onOptionsItemSelected(item)
}
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
val f = permissionRequestCallbacks[requestCode]
if (f != null) {
permissionRequestCallbacks.remove(requestCode)
f.invoke(permissions, grantResults)
}
}
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
addPreferencesFromResource(R.xml.preferences)

View File

@ -4,9 +4,7 @@
*/
package com.wireguard.android.preference
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.util.AttributeSet
import android.util.Log
import androidx.preference.Preference
@ -43,7 +41,13 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
if (configs.isEmpty()) {
throw IllegalArgumentException(context.getString(R.string.no_tunnels_error))
}
val outputFile = DownloadsFileSaver.save(context, "wireguard-export.zip", "application/zip", true)
val outputFile = DownloadsFileSaver.save(activity, "wireguard-export.zip", "application/zip", true)
if (outputFile == null) {
withContext(Dispatchers.Main.immediate) {
isEnabled = true
}
return@withContext null
}
try {
ZipOutputStream(outputFile.outputStream).use { zip ->
for (i in configs.indices) {
@ -82,17 +86,8 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
when (it) {
// When we have successful authentication, or when there is no biometric hardware available.
is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
if (DownloadsFileSaver.needsWriteExternalStoragePermission) {
activity.ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
isEnabled = false
exportZip()
}
}
} else {
isEnabled = false
exportZip()
}
isEnabled = false
exportZip()
}
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(

View File

@ -4,67 +4,88 @@
*/
package com.wireguard.android.util
import android.Manifest
import android.content.ContentValues
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.wireguard.android.R
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
object DownloadsFileSaver {
val needsWriteExternalStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
@Throws(Exception::class)
fun save(context: Context, name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentResolver = context.contentResolver
if (overwriteExisting)
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), arrayOf(name))
val contentValues = ContentValues()
contentValues.put(MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
val contentStream = contentResolver.openOutputStream(contentUri)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
var path: String? = null
if (cursor != null) {
try {
if (cursor.moveToFirst())
path = cursor.getString(0)
} finally {
cursor.close()
}
}
if (path == null) {
path = "Download/"
cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DISPLAY_NAME), null, null, null)
suspend fun save(context: ComponentActivity, name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
withContext(Dispatchers.IO) {
val contentResolver = context.contentResolver
if (overwriteExisting)
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), arrayOf(name))
val contentValues = ContentValues()
contentValues.put(MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
val contentStream = contentResolver.openOutputStream(contentUri)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
var path: String? = null
if (cursor != null) {
try {
if (cursor.moveToFirst())
path += cursor.getString(0)
path = cursor.getString(0)
} finally {
cursor.close()
}
}
if (path == null) {
path = "Download/"
cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DISPLAY_NAME), null, null, null)
if (cursor != null) {
try {
if (cursor.moveToFirst())
path += cursor.getString(0)
} finally {
cursor.close()
}
}
}
DownloadsFile(context, contentStream, path, contentUri)
}
DownloadsFile(context, contentStream, path, contentUri)
} else {
@Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(path, name)
if (!path.isDirectory && !path.mkdirs())
throw IOException(context.getString(R.string.create_output_dir_error))
DownloadsFile(context, FileOutputStream(file), file.absolutePath, null)
withContext(Dispatchers.Main.immediate) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
val futureGrant = CompletableDeferred<Boolean>()
val activityResult = context.registerForActivityResult(ActivityResultContracts.RequestPermission(), futureGrant::complete)
activityResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
val granted = futureGrant.await()
activityResult.unregister()
if (!granted)
return@withContext null
}
@Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
withContext(Dispatchers.IO) {
val file = File(path, name)
if (!path.isDirectory && !path.mkdirs())
throw IOException(context.getString(R.string.create_output_dir_error))
DownloadsFile(context, FileOutputStream(file), file.absolutePath, null)
}
}
}
class DownloadsFile(private val context: Context, val outputStream: OutputStream, val fileName: String, private val uri: Uri?) {
fun delete() {
suspend fun delete() = withContext(Dispatchers.IO) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
context.contentResolver.delete(uri!!, null, null)
else