diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4735783704..534d0536ff 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -40,6 +40,12 @@
+
+
+
+
+
+
{
+
+ protected var fragmentOpenFileDialogBinding: FragmentOpenFileDialogBinding? = null
+ protected val viewBinding get() = fragmentOpenFileDialogBinding!!
+
+ private lateinit var adapter: AppsRecyclerAdapter
+ private lateinit var sharedPreferences: SharedPreferences
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setStyle(STYLE_NORMAL, R.style.appBottomSheetDialogTheme)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ fragmentOpenFileDialogBinding = FragmentOpenFileDialogBinding.inflate(inflater)
+ initDialogResources(viewBinding.parent)
+ return viewBinding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val modelProvider = AppsAdapterPreloadModel(this, true)
+ val sizeProvider = ViewPreloadSizeProvider()
+ val preloader = RecyclerViewPreloader(
+ Glide.with(this),
+ modelProvider,
+ sizeProvider,
+ GlideConstants.MAX_PRELOAD_FILES,
+ )
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
+
+ val appDataParcelableList = loadAppList()
+
+ adapter = AppsRecyclerAdapter(
+ this,
+ modelProvider,
+ true,
+ this,
+ appDataParcelableList,
+ )
+ loadViews()
+ viewBinding.appsRecyclerView.addOnScrollListener(preloader)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ fragmentOpenFileDialogBinding = null
+ }
+
+ override fun onPause() {
+ super.onPause()
+ dismiss()
+ }
+
+ private fun loadViews() {
+ viewBinding.run {
+ appsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
+ appsRecyclerView.adapter = adapter
+ doLoadViewsWith(this)
+ }
+ }
+
+ protected open fun doLoadViewsWith(viewBinding: FragmentOpenFileDialogBinding) = Unit
+
+ protected abstract fun loadAppList(): MutableList
+
+ protected abstract fun initLastAppData(
+ lastClassAndPackage: List?,
+ appDataParcelableList: MutableList,
+ ): AppDataParcelable?
+
+ override fun adjustListViewForTv(viewHolder: AppHolder, mainActivity: MainActivity) {
+ // do nothing
+ }
+}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/OpenFolderInTerminalFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/OpenFolderInTerminalFragment.kt
new file mode 100644
index 0000000000..c183df181d
--- /dev/null
+++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/OpenFolderInTerminalFragment.kt
@@ -0,0 +1,265 @@
+package com.amaze.filemanager.ui.dialogs
+
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.preference.PreferenceManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.amaze.filemanager.R
+import com.amaze.filemanager.adapters.AppsRecyclerAdapter
+import com.amaze.filemanager.adapters.data.AppDataParcelable
+import com.amaze.filemanager.adapters.glide.AppsAdapterPreloadModel
+import com.amaze.filemanager.adapters.holders.AppHolder
+import com.amaze.filemanager.application.AppConfig
+import com.amaze.filemanager.databinding.FragmentOpenFileDialogBinding
+import com.amaze.filemanager.ui.activities.MainActivity
+import com.amaze.filemanager.ui.activities.superclasses.PreferenceActivity
+import com.amaze.filemanager.ui.activities.superclasses.ThemedActivity
+import com.amaze.filemanager.ui.base.BaseBottomSheetFragment
+import com.amaze.filemanager.ui.fragments.AdjustListViewForTv
+import com.amaze.filemanager.utils.GlideConstants
+import com.amaze.filemanager.utils.detectInstalledTerminalApps
+import com.bumptech.glide.Glide
+import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
+import com.bumptech.glide.util.ViewPreloadSizeProvider
+import org.slf4j.LoggerFactory
+
+class OpenFolderInTerminalFragment : BaseBottomSheetFragment(), AdjustListViewForTv {
+
+ private var fragmentOpenFileDialogBinding: FragmentOpenFileDialogBinding? = null
+ private val viewBinding get() = fragmentOpenFileDialogBinding!!
+
+ private lateinit var path: String
+ private lateinit var installedTerminals: Array
+ private lateinit var adapter: AppsRecyclerAdapter
+ private lateinit var sharedPreferences: SharedPreferences
+
+ companion object {
+
+ private val logger = LoggerFactory.getLogger(OpenFileDialogFragment::class.java)
+
+ private const val KEY_PREFERENCES_DEFAULT = "terminal._DEFAULT"
+ const val KEY_PREFERENCES_LAST = "terminal._LAST"
+
+ fun openTerminalOrShow(path: String, activity: MainActivity) {
+ val installedTerminals = activity.detectInstalledTerminalApps()
+ if (installedTerminals.isEmpty()) {
+ AppConfig.toast(activity, "No Terminal App installed")
+// } else if (installedTerminals.size == 1) {
+// activity.triggerOpenFolderInTerminalIntent(installedTerminals.first(), path)
+// } else {
+ } else {
+ newInstance(path, installedTerminals).show(activity.supportFragmentManager, javaClass.simpleName)
+ }
+ }
+
+ private fun newInstance(path: String, installedTerminals: Array):
+ OpenFolderInTerminalFragment {
+ val retval = OpenFolderInTerminalFragment()
+ retval.path = path
+ retval.installedTerminals = installedTerminals
+ retval.arguments = Bundle().also {
+ it.putString("path", path)
+ }
+ return retval
+ }
+
+ /**
+ * Sets last open app preference for bottom sheet file chooser.
+ * Next time same mime type comes, this app will be shown on top of the list if present
+ */
+ fun setLastOpenedApp(
+ appDataParcelable: AppDataParcelable,
+ preferenceActivity: PreferenceActivity,
+ ) {
+ preferenceActivity.prefs.edit().putString(
+ KEY_PREFERENCES_LAST,
+ String.format(
+ "%s %s",
+ appDataParcelable.openFileParcelable?.className,
+ appDataParcelable.openFileParcelable?.packageName,
+ ),
+ ).apply()
+ }
+
+ /**
+ * Sets default app for mime type selected using 'Always' button from bottom sheet
+ */
+ private fun setDefaultOpenedApp(
+ appDataParcelable: AppDataParcelable,
+ preferenceActivity: PreferenceActivity,
+ ) {
+ preferenceActivity.prefs.edit().putString(
+ KEY_PREFERENCES_DEFAULT,
+ String.format(
+ "%s %s",
+ appDataParcelable.openFileParcelable?.className,
+ appDataParcelable.openFileParcelable?.packageName,
+ ),
+ ).apply()
+ }
+
+ /**
+ * Clears all default apps set preferences for mime types
+ */
+ fun clearPreferences(sharedPreferences: SharedPreferences) {
+ AppConfig.getInstance().runInBackground {
+ val keys = HashSet()
+ sharedPreferences.all.keys.forEach {
+ if (it.endsWith(KEY_PREFERENCES_DEFAULT) ||
+ it.endsWith(KEY_PREFERENCES_LAST)
+ ) {
+ keys.add(it)
+ }
+ }
+ keys.forEach {
+ sharedPreferences.edit().remove(it).apply()
+ }
+ }
+ }
+
+ private fun clearMimeTypePreference(
+ mimeType: String,
+ sharedPreferences: SharedPreferences,
+ ) {
+ sharedPreferences.edit().remove(KEY_PREFERENCES_DEFAULT).apply()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setStyle(STYLE_NORMAL, R.style.appBottomSheetDialogTheme)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ fragmentOpenFileDialogBinding = FragmentOpenFileDialogBinding.inflate(inflater)
+ initDialogResources(viewBinding.parent)
+ return viewBinding.root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ fragmentOpenFileDialogBinding = null
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val modelProvider = AppsAdapterPreloadModel(this, true)
+ val sizeProvider = ViewPreloadSizeProvider()
+ val preloader = RecyclerViewPreloader(
+ Glide.with(this),
+ modelProvider,
+ sizeProvider,
+ GlideConstants.MAX_PRELOAD_FILES,
+ )
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
+
+ val appDataParcelableList = initList()
+ val lastClassAndPackageRaw = sharedPreferences
+ .getString("terminal.${OpenFileDialogFragment.KEY_PREFERENCES_LAST}", null)
+ val lastClassAndPackage = lastClassAndPackageRaw?.split(" ")
+ val lastAppData: AppDataParcelable = initLastAppData(
+ lastClassAndPackage,
+ appDataParcelableList,
+ ) ?: return
+
+ adapter = AppsRecyclerAdapter(
+ this,
+ modelProvider,
+ true,
+ this,
+ appDataParcelableList,
+ )
+ loadViews(lastAppData)
+ viewBinding.appsRecyclerView.addOnScrollListener(preloader)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ dismiss()
+ }
+
+ private fun initList(): MutableList {
+ val packageManager = requireContext().packageManager
+ val appDataParcelableList: MutableList = ArrayList()
+ for (pkg in installedTerminals) {
+ kotlin.runCatching { packageManager.getPackageInfo(pkg, 0) }
+ .onFailure {
+ logger.error("Error getting package info for $pkg", it)
+ }.getOrNull()?.run {
+ packageManager.getApplicationInfo(pkg, 0).let { applicationInfo ->
+ appDataParcelableList.add(
+ AppDataParcelable(
+ packageManager.getApplicationLabel(applicationInfo).toString(),
+ "",
+ null,
+ this.packageName,
+ "",
+ "",
+ 0,
+ 0, false,
+ null,
+ ),
+ )
+ }
+ }
+ }
+ return appDataParcelableList
+ }
+
+ private fun initLastAppData(
+ lastClassAndPackage: List?,
+ appDataParcelableList: MutableList,
+ ): AppDataParcelable? {
+ if (appDataParcelableList.size == 0) {
+ AppConfig.toast(requireContext(), "No terminal apps available")
+ dismiss()
+ return null
+ }
+
+ if (appDataParcelableList.size == 1) {
+ }
+
+ var lastAppData: AppDataParcelable? = if (!lastClassAndPackage.isNullOrEmpty()) {
+ appDataParcelableList.find {
+ it.openFileParcelable?.className == lastClassAndPackage[0]
+ }
+ } else {
+ null
+ }
+ lastAppData = lastAppData ?: appDataParcelableList[0]
+ appDataParcelableList.remove(lastAppData)
+ return lastAppData
+ }
+
+ private fun loadViews(lastAppData: AppDataParcelable) {
+ lastAppData.let {
+ viewBinding.run {
+ appsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
+ appsRecyclerView.adapter = adapter
+
+ justOnceButton.setTextColor((activity as ThemedActivity).accent)
+ justOnceButton.setOnClickListener { _ ->
+ setLastOpenedApp(it, activity as PreferenceActivity)
+// requireContext().startActivityCatchingSecurityException(lastAppIntent)
+ }
+ alwaysButton.setTextColor((activity as ThemedActivity).accent)
+ alwaysButton.setOnClickListener { _ ->
+ setDefaultOpenedApp(it, activity as PreferenceActivity)
+// requireContext().startActivityCatchingSecurityException(lastAppIntent)
+ }
+ }
+ }
+ }
+
+ override fun adjustListViewForTv(viewHolder: AppHolder, mainActivity: MainActivity) {
+ // do nothing
+ }
+}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java
index 29c2a3393b..ebf9497529 100644
--- a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java
+++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java
@@ -85,6 +85,7 @@
import com.amaze.filemanager.utils.Utils;
import com.google.android.material.appbar.AppBarLayout;
+import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipDescription;
diff --git a/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt b/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt
index 4af20b454a..d3ceb1ccd0 100644
--- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt
+++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt
@@ -38,6 +38,7 @@ import com.amaze.filemanager.filesystem.PasteHelper
import com.amaze.filemanager.filesystem.files.FileUtils
import com.amaze.filemanager.ui.activities.MainActivity
import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation
+import com.amaze.filemanager.ui.dialogs.OpenFolderInTerminalFragment
import com.amaze.filemanager.ui.selection.SelectionPopupMenu.Companion.invokeSelectionDropdown
import java.io.File
import java.lang.ref.WeakReference
@@ -399,6 +400,10 @@ class MainActivityActionMode(private val mainActivityReference: WeakReference {
+ OpenFolderInTerminalFragment.openTerminalOrShow(checkedItems[0].desc, mainActivity)
+ true
+ }
else -> false
}
}
diff --git a/app/src/main/java/com/amaze/filemanager/utils/OpenTerminalUtilsExt.kt b/app/src/main/java/com/amaze/filemanager/utils/OpenTerminalUtilsExt.kt
new file mode 100644
index 0000000000..4a1676fb34
--- /dev/null
+++ b/app/src/main/java/com/amaze/filemanager/utils/OpenTerminalUtilsExt.kt
@@ -0,0 +1,67 @@
+package com.amaze.filemanager.utils
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import androidx.core.content.ContextCompat
+import com.amaze.filemanager.ui.activities.MainActivity
+
+const val TERMONE_PLUS = "com.termoneplus"
+const val ANDROID_TERM = "jackpal.androidterm"
+const val MATERIAL_TERMINAL = "yarolegovich.materialterminal"
+const val TERMUX = "com.termux"
+
+fun MainActivity.triggerOpenFolderInTerminalIntent(pkgName: String, path: String) {
+ when (pkgName) {
+ TERMONE_PLUS -> {
+ Intent().also {
+ it.action = "$TERMONE_PLUS.RUN_SCRIPT"
+ it.setClassName(TERMONE_PLUS, "$ANDROID_TERM.RunScript")
+ it.putExtra("$TERMONE_PLUS.Command", "cd \"$path\"")
+ startActivity(intent)
+ }
+ }
+
+ ANDROID_TERM -> {
+ Intent().also {
+ it.action = "$ANDROID_TERM.RUN_SCRIPT"
+ it.setClassName(ANDROID_TERM, "$ANDROID_TERM.RunScript")
+ it.putExtra("$ANDROID_TERM.iInitialCommand", "cd \"$path\"")
+ startActivity(intent)
+ }
+ }
+
+ TERMUX -> {
+ Intent().also {
+ it.setClassName("com.termux", "com.termux.app.RunCommandService")
+ it.setAction("com.termux.RUN_COMMAND")
+ it.putExtra(
+ "com.termux.RUN_COMMAND_PATH",
+ "/data/data/com.termux/files/usr/bin/bash"
+ );
+ it.putExtra("com.termux.RUN_COMMAND_WORKDIR", path);
+ ContextCompat.startForegroundService(this, it)
+ }
+ }
+
+ else -> {
+ throw IllegalArgumentException("Unsupported package: $pkgName")
+ }
+ }
+}
+
+fun MainActivity.detectInstalledTerminalApps(): Array {
+ val retval = ArrayList()
+
+ for (pkg in arrayOf(TERMONE_PLUS, ANDROID_TERM, TERMUX, MATERIAL_TERMINAL)) {
+ packageManager.getLaunchIntentForPackage(pkg)?.run {
+ if (packageManager.queryIntentActivities(
+ this,
+ PackageManager.MATCH_DEFAULT_ONLY,
+ ).isNotEmpty()
+ ) {
+ retval.add(pkg)
+ }
+ }
+ }
+ return retval.toTypedArray()
+}
diff --git a/app/src/main/res/menu/activity_extra.xml b/app/src/main/res/menu/activity_extra.xml
index 0c601834d7..5bc39b53fd 100644
--- a/app/src/main/res/menu/activity_extra.xml
+++ b/app/src/main/res/menu/activity_extra.xml
@@ -55,6 +55,9 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7d3f97ae2a..5ff381465a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -71,6 +71,7 @@
About
Extract
Compress
+ Open in Terminal
Yes
No
ⓘ