Add SAF permission grant routine for stock installs

Fixes #2592.

- Refactored FtpServerFragment to allow different ActivityResultLauncher instances of different callbacks to be created in same code
- Added routine to prompt user to grant access to device's internal storage (default FTP server shared folder) at SAF level
This commit is contained in:
Raymond Lai
2021-06-08 23:14:08 +08:00
parent 863f601afd
commit 6683479de5
3 changed files with 106 additions and 25 deletions
@@ -35,6 +35,7 @@ import android.os.Build.VERSION_CODES.M
import android.os.Environment
import android.os.IBinder
import android.os.SystemClock
import android.provider.DocumentsContract
import androidx.preference.PreferenceManager
import com.amaze.filemanager.R
import com.amaze.filemanager.application.AppConfig
@@ -242,7 +243,10 @@ class FtpService : Service(), Runnable {
val DEFAULT_PATH: String = if (Build.VERSION.SDK_INT < M) {
Environment.getExternalStorageDirectory().absolutePath
} else {
"content://com.android.externalstorage.documents/tree/primary%3A"
DocumentsContract.buildTreeDocumentUri(
"com.android.externalstorage.documents",
"primary:"
).toString()
}
// RequestStartStopReceiver listens for these actions to start/stop this server
@@ -25,19 +25,23 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Build.VERSION_CODES.M
import android.os.Build.VERSION_CODES.*
import android.os.Bundle
import android.os.Environment
import android.os.Process
import android.provider.DocumentsContract
import android.provider.DocumentsContract.EXTRA_INITIAL_URI
import android.provider.Settings
import android.text.InputType
import android.text.Spanned
import android.view.*
import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.text.HtmlCompat
@@ -50,6 +54,8 @@ import com.afollestad.materialdialogs.folderselector.FolderChooserDialog
import com.amaze.filemanager.R
import com.amaze.filemanager.application.AppConfig
import com.amaze.filemanager.asynchronous.services.ftp.FtpService
import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.DEFAULT_PATH
import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.KEY_PREFERENCE_PATH
import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.getLocalInetAddress
import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.isConnectedToLocalNetwork
import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.isConnectedToWifi
@@ -101,22 +107,17 @@ class FtpServerFragment : Fragment(R.layout.fragment_ftp) {
private val mainActivity: MainActivity get() = requireActivity() as MainActivity
@Suppress("LabeledExpression")
private val activityResultHandler = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == RESULT_OK && Build.VERSION.SDK_INT >= LOLLIPOP) {
val directoryUri = it.data?.data ?: return@registerForActivityResult
requireContext().contentResolver.takePersistableUriPermission(
directoryUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
changeFTPServerPath(directoryUri.toString())
updatePathText()
}
private val activityResultHandlerOnFtpServerPathUpdate = createOpenDocumentTreeIntentCallback {
directoryUri ->
changeFTPServerPath(directoryUri.toString())
updatePathText()
}
private val activityResultHandlerOnFtpServerPathGrantedSafAccess =
createOpenDocumentTreeIntentCallback {
doStartServer()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@@ -171,7 +172,7 @@ class FtpServerFragment : Fragment(R.layout.fragment_ftp) {
true
) { _: MaterialDialog?, _: CharSequence? -> }
.inputType(InputType.TYPE_CLASS_NUMBER)
.onPositive { dialog: MaterialDialog, which: DialogAction? ->
.onPositive { dialog: MaterialDialog, _: DialogAction? ->
val editText = dialog.inputEditText
if (editText != null) {
val name = editText.text.toString()
@@ -200,7 +201,9 @@ class FtpServerFragment : Fragment(R.layout.fragment_ftp) {
}
R.id.ftp_path -> {
if (Build.VERSION.SDK_INT >= M) {
activityResultHandler.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
activityResultHandlerOnFtpServerPathUpdate.launch(
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
)
} else {
val dialogBuilder = FolderChooserDialog.Builder(requireActivity())
dialogBuilder
@@ -220,7 +223,7 @@ class FtpServerFragment : Fragment(R.layout.fragment_ftp) {
val loginDialogView =
DialogFtpLoginBinding.inflate(LayoutInflater.from(requireContext())).apply {
initLoginDialogViews(this)
loginDialogBuilder.onPositive { dialog: MaterialDialog, _: DialogAction ->
loginDialogBuilder.onPositive { _: MaterialDialog, _: DialogAction ->
if (checkboxFtpAnonymous.isChecked) {
// remove preferences
setFTPUsername("")
@@ -364,12 +367,75 @@ class FtpServerFragment : Fragment(R.layout.fragment_ftp) {
updateStatus()
}
@Suppress("LabeledExpression")
private fun createOpenDocumentTreeIntentCallback(callback: (directoryUri: Uri) -> Unit):
ActivityResultLauncher<Intent> {
return registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == RESULT_OK && Build.VERSION.SDK_INT >= LOLLIPOP) {
val directoryUri = it.data?.data ?: return@registerForActivityResult
requireContext().contentResolver.takePersistableUriPermission(
directoryUri, GRANT_URI_RW_PERMISSION
)
callback.invoke(directoryUri)
}
}
}
/** Check URI access if */
private fun checkUriAccessIfNecessary(callback: () -> Unit) {
if (Build.VERSION.SDK_INT >= M) {
val directoryUri = mainActivity.prefs.getString(KEY_PREFERENCE_PATH, DEFAULT_PATH)
Uri.parse(directoryUri).run {
if (requireContext().checkUriPermission(
this, Process.myPid(), Process.myUid(),
GRANT_URI_RW_PERMISSION
) == PackageManager.PERMISSION_DENIED
) {
mainActivity.accent.run {
MaterialDialog.Builder(mainActivity)
.content(R.string.ftp_prompt_accept_first_start_saf_access)
.widgetColor(accentColor)
.theme(mainActivity.appTheme.materialDialogTheme)
.title(R.string.ftp_prompt_accept_first_start_saf_access_title)
.positiveText(R.string.ok)
.positiveColor(accentColor)
.negativeText(R.string.cancel)
.negativeColor(accentColor)
.onPositive { dialog, _ ->
activityResultHandlerOnFtpServerPathGrantedSafAccess.launch(
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).also {
if (Build.VERSION.SDK_INT >= O &&
directoryUri.equals(DEFAULT_PATH)
) {
it.putExtra(
EXTRA_INITIAL_URI,
DocumentsContract.buildDocumentUri(
"com.android.externalstorage.documents",
"primary"
)
)
}
}
)
dialog.dismiss()
}.build().show()
}
} else {
callback.invoke()
}
}
} else {
callback.invoke()
}
}
/** Sends a broadcast to start ftp server */
private fun startServer() {
requireContext().sendBroadcast(
Intent(FtpService.ACTION_START_FTPSERVER)
.setPackage(requireContext().packageName)
)
checkUriAccessIfNecessary {
doStartServer()
}
}
/** Sends a broadcast to stop ftp server */
@@ -380,6 +446,11 @@ class FtpServerFragment : Fragment(R.layout.fragment_ftp) {
)
}
private fun doStartServer() = requireContext().sendBroadcast(
Intent(FtpService.ACTION_START_FTPSERVER)
.setPackage(requireContext().packageName)
)
override fun onResume() {
super.onResume()
val wifiFilter = IntentFilter()
@@ -433,7 +504,7 @@ class FtpServerFragment : Fragment(R.layout.fragment_ftp) {
} else {
View.VISIBLE
}
ftpPasswordVisibleButton.setOnClickListener { v: View? ->
ftpPasswordVisibleButton.setOnClickListener {
if (password.text.toString().contains("\u25CF")) {
// password was not visible, let's make it visible
password.text = resources.getString(R.string.password) + ": " + passwordDecrypted
@@ -728,5 +799,7 @@ class FtpServerFragment : Fragment(R.layout.fragment_ftp) {
companion object {
const val TAG = "FtpServerFragment"
const val REQUEST_CODE_SAF_FTP = 225
const val GRANT_URI_RW_PERMISSION =
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
}
}
+4
View File
@@ -707,5 +707,9 @@
<string name="disable">Disable</string>
<string name="choose_operation">Choose operation to perform</string>
<string name="remember">Remember for next time</string>
<string name="ftp_prompt_accept_first_start_saf_access_title">Grant SAF access for FTP server</string>
<string name="ftp_prompt_accept_first_start_saf_access">Since this is your first run of the FTP server, and you are using the device\'s internal storage as shared folder, please allow Amaze to access the storage using SAF.\n\n
You only need to do this once, until the next time you select a new location for sharing.</string>
</resources>