diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ftp/FtpService.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ftp/FtpService.kt index ac4dc9b7e..58c2ee501 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ftp/FtpService.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ftp/FtpService.kt @@ -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 diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt index a3d89e79a..93a943e79 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt @@ -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 { + 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 } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3c64262bc..5b9740a5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -707,5 +707,9 @@ Disable Choose operation to perform Remember for next time + Grant SAF access for FTP server + 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.