diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4735783704..1b6e8623ce 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -40,6 +40,9 @@
+
+
+
,
private val appDataParcelableList: MutableList,
+ // Optional, for specifying customized action on row click
+ private val onClickRowAction: ((AppDataParcelable) -> Unit)? = null,
) : RecyclerView.Adapter() {
private val myChecked = SparseBooleanArray()
private var appDataListItem: MutableList = mutableListOf()
@@ -209,7 +214,11 @@ class AppsRecyclerAdapter(
holder.rl.isClickable = true
holder.rl.nextFocusRightId = holder.about.id
holder.rl.setOnClickListener {
- startActivityForRowItem(rowItem)
+ if (onClickRowAction != null) {
+ onClickRowAction.invoke(rowItem)
+ } else {
+ startActivityForRowItem(rowItem)
+ }
}
}
if (myChecked[position]) {
@@ -508,7 +517,7 @@ class AppsRecyclerAdapter(
MaterialDialog.Builder(fragment.requireContext())
builder1
.theme(
- themedActivity.appTheme.getMaterialDialogTheme(),
+ themedActivity.appTheme.materialDialogTheme,
)
.content(fragment.getString(R.string.unin_system_apk))
.title(fragment.getString(R.string.warning))
diff --git a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java
index ace6284542..22a2d74d50 100644
--- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java
+++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java
@@ -1435,6 +1435,7 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl
if (rowItem.isDirectory) {
popupMenu.getMenu().findItem(R.id.open_with).setVisible(false);
popupMenu.getMenu().findItem(R.id.share).setVisible(false);
+ popupMenu.getMenu().findItem(R.id.open_in_terminal).setVisible(true);
if (mainFragment.getMainActivity().mReturnIntent) {
popupMenu.getMenu().findItem(R.id.return_select).setVisible(true);
@@ -1442,6 +1443,7 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl
} else {
popupMenu.getMenu().findItem(R.id.book).setVisible(false);
popupMenu.getMenu().findItem(R.id.compress).setVisible(true);
+ popupMenu.getMenu().findItem(R.id.open_in_terminal).setVisible(false);
if (description.endsWith(fileExtensionZip)
|| description.endsWith(fileExtensionJar)
diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java
index ac9dc8b25d..0326b4f03f 100644
--- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java
+++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java
@@ -116,6 +116,8 @@
import io.reactivex.schedulers.Schedulers;
import jcifs.smb.SmbException;
import jcifs.smb.SmbFile;
+import kotlin.Deprecated;
+import kotlin.ReplaceWith;
import kotlin.collections.ArraysKt;
import kotlin.io.ByteStreamsKt;
import kotlin.text.Charsets;
@@ -607,6 +609,9 @@ public String getParent(Context context) {
*
* @deprecated use {@link #isDirectory(Context)} to handle content resolvers
*/
+ @Deprecated(
+ replaceWith = @ReplaceWith(expression = "isDirectory(Context)", imports = ""),
+ message = "")
public boolean isDirectory() {
boolean isDirectory;
switch (mode) {
@@ -701,6 +706,9 @@ public Boolean execute(@NonNull SFTPClient client) {
/**
* @deprecated use {@link #folderSize(Context)}
*/
+ @Deprecated(
+ replaceWith = @ReplaceWith(expression = "folderSize(Context)", imports = ""),
+ message = "")
public long folderSize() {
long size = 0L;
@@ -1060,6 +1068,9 @@ public FTPFile[] executeWithFtpClient(@NonNull FTPClient ftpClient)
*
* @deprecated use forEachChildrenFile()
*/
+ @Deprecated(
+ replaceWith = @ReplaceWith(expression = "forEachChildrenFile", imports = ""),
+ message = "")
public ArrayList listFiles(Context context, boolean isRoot) {
ArrayList arrayList = new ArrayList<>();
forEachChildrenFile(context, isRoot, arrayList::add);
diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.java b/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.java
index bc7bb48d8e..f38e89aced 100644
--- a/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.java
+++ b/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.java
@@ -61,6 +61,9 @@
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
+import kotlin.Deprecated;
+import kotlin.ReplaceWith;
+
/**
* Created by vishal on 19/4/17.
*
@@ -73,6 +76,7 @@ public class CloudUtil {
/**
* @deprecated use getCloudFiles()
*/
+ @Deprecated(replaceWith = @ReplaceWith(expression = "getCloudFiles", imports = ""), message = "")
public static ArrayList listFiles(
String path, CloudStorage cloudStorage, OpenMode openMode) throws CloudPluginException {
final ArrayList baseFiles = new ArrayList<>();
diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java
index d9bdd010a7..a17c77c552 100644
--- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java
+++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java
@@ -377,7 +377,11 @@ public static void installApk(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& !permissionsActivity.getPackageManager().canRequestPackageInstalls()) {
permissionsActivity.requestInstallApkPermission(
- () -> installApk(f, permissionsActivity), true);
+ () -> {
+ installApk(f, permissionsActivity);
+ return null;
+ },
+ true);
}
Intent intent = new Intent(Intent.ACTION_VIEW);
diff --git a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java
index 2320ddb3c4..a965629619 100644
--- a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java
+++ b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java
@@ -37,6 +37,7 @@
import com.amaze.filemanager.ui.dialogs.EncryptAuthenticateDialog;
import com.amaze.filemanager.ui.dialogs.EncryptWithPresetPasswordSaveAsDialog;
import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation;
+import com.amaze.filemanager.ui.dialogs.OpenFolderInTerminalFragment;
import com.amaze.filemanager.ui.fragments.MainFragment;
import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants;
import com.amaze.filemanager.ui.provider.UtilitiesProvider;
@@ -256,6 +257,9 @@ public void onButtonPressed(Intent intent, String password)
case R.id.return_select:
mainFragment.returnIntentResults(new HybridFileParcelable[] {rowItem.generateBaseFile()});
return true;
+ case R.id.open_in_terminal:
+ OpenFolderInTerminalFragment.Companion.openTerminalOrShow(rowItem.desc, mainActivity);
+ return true;
}
return false;
}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java
index fe7dec1436..77714b220c 100644
--- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java
+++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java
@@ -210,7 +210,9 @@
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
+import kotlin.Unit;
import kotlin.collections.ArraysKt;
+import kotlin.jvm.functions.Function0;
import kotlin.jvm.functions.Function1;
import kotlin.text.Charsets;
@@ -220,7 +222,7 @@ public class MainActivity extends PermissionsActivity
CloudConnectionCallbacks,
LoaderManager.LoaderCallbacks,
FolderChooserDialog.FolderCallback,
- PermissionsActivity.OnPermissionGranted {
+ Function0 {
private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class);
@@ -530,9 +532,8 @@ public void invalidateFragmentAndBundle(Bundle savedInstanceState, boolean isClo
}
}
- @Override
@SuppressLint("CheckResult")
- public void onPermissionGranted() {
+ public Unit invoke() {
drawer.refreshDrawer();
TabFragment tabFragment = getTabFragment();
boolean b = getBoolean(PREFERENCE_NEED_TO_SET_HOME);
@@ -561,6 +562,7 @@ public void onPermissionGranted() {
if (main1 != null) ((MainFragment) main1).updateList(false);
}
}
+ return null;
}
private void checkForExternalPermission() {
@@ -1120,6 +1122,7 @@ public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.hiddenitems).setVisible(true);
menu.findItem(R.id.view).setVisible(true);
menu.findItem(R.id.extract).setVisible(false);
+ menu.findItem(R.id.open_in_terminal).setVisible(true);
invalidatePasteSnackbar(true);
findViewById(R.id.buttonbarframe).setVisibility(View.VISIBLE);
} else if (fragment instanceof AppsListFragment
@@ -1133,6 +1136,7 @@ public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.home).setVisible(false);
menu.findItem(R.id.history).setVisible(false);
menu.findItem(R.id.extract).setVisible(false);
+ menu.findItem(R.id.open_in_terminal).setVisible(false);
if (fragment instanceof ProcessViewerFragment) {
menu.findItem(R.id.sort).setVisible(false);
} else if (fragment instanceof FtpServerFragment) {
@@ -1156,6 +1160,7 @@ public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.hiddenitems).setVisible(false);
menu.findItem(R.id.view).setVisible(false);
menu.findItem(R.id.extract).setVisible(true);
+ menu.findItem(R.id.open_in_terminal).setVisible(false);
invalidatePasteSnackbar(false);
}
return super.onPrepareOptionsMenu(menu);
@@ -1291,6 +1296,10 @@ public boolean onOptionsItemSelected(MenuItem item) {
break;
case R.id.search:
getAppbar().getSearchView().revealSearchView();
+ break;
+ case R.id.open_in_terminal:
+ if (getFragmentAtFrame() instanceof MainFragment) {}
+
break;
}
return null;
diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/BasicActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/BasicActivity.kt
similarity index 52%
rename from app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/BasicActivity.java
rename to app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/BasicActivity.kt
index a5ebc37afc..59aeabdc58 100644
--- a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/BasicActivity.java
+++ b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/BasicActivity.kt
@@ -17,32 +17,25 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+package com.amaze.filemanager.ui.activities.superclasses
-package com.amaze.filemanager.ui.activities.superclasses;
+import androidx.appcompat.app.AppCompatActivity
+import com.amaze.filemanager.application.AppConfig
+import com.amaze.filemanager.ui.colors.ColorPreferenceHelper
+import com.amaze.filemanager.ui.provider.UtilitiesProvider
+import com.amaze.filemanager.ui.theme.AppTheme
-import com.amaze.filemanager.application.AppConfig;
-import com.amaze.filemanager.ui.colors.ColorPreferenceHelper;
-import com.amaze.filemanager.ui.provider.UtilitiesProvider;
-import com.amaze.filemanager.ui.theme.AppTheme;
+/** Created by rpiotaix on 17/10/16. */
+open class BasicActivity : AppCompatActivity() {
+ private val appConfig: AppConfig
+ get() = application as AppConfig
-import androidx.appcompat.app.AppCompatActivity;
+ val colorPreference: ColorPreferenceHelper
+ get() = appConfig.utilsProvider.colorPreference
-/** Created by rpiotaix on 17/10/16. */
-public class BasicActivity extends AppCompatActivity {
+ val appTheme: AppTheme
+ get() = appConfig.utilsProvider.appTheme
- protected AppConfig getAppConfig() {
- return (AppConfig) getApplication();
- }
-
- public ColorPreferenceHelper getColorPreference() {
- return getAppConfig().getUtilsProvider().getColorPreference();
- }
-
- public AppTheme getAppTheme() {
- return getAppConfig().getUtilsProvider().getAppTheme();
- }
-
- public UtilitiesProvider getUtilsProvider() {
- return getAppConfig().getUtilsProvider();
- }
+ val utilsProvider: UtilitiesProvider
+ get() = appConfig.utilsProvider
}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java
deleted file mode 100644
index 821f433420..0000000000
--- a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java
+++ /dev/null
@@ -1,297 +0,0 @@
-/*
- * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
- * Emmanuel Messulam, Raymond Lai and Contributors.
- *
- * This file is part of Amaze File Manager.
- *
- * Amaze File Manager is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.amaze.filemanager.ui.activities.superclasses;
-
-import static android.os.Build.VERSION.SDK_INT;
-import static android.os.Build.VERSION_CODES.TIRAMISU;
-
-import com.afollestad.materialdialogs.DialogAction;
-import com.afollestad.materialdialogs.MaterialDialog;
-import com.amaze.filemanager.R;
-import com.amaze.filemanager.application.AppConfig;
-import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation;
-import com.amaze.filemanager.utils.Utils;
-import com.google.android.material.snackbar.BaseTransientBottomBar;
-import com.google.android.material.snackbar.Snackbar;
-
-import android.Manifest;
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Environment;
-import android.provider.Settings;
-import android.util.Log;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.core.app.ActivityCompat;
-
-public class PermissionsActivity extends ThemedActivity
- implements ActivityCompat.OnRequestPermissionsResultCallback {
-
- private static final String TAG = PermissionsActivity.class.getSimpleName();
-
- public static final int PERMISSION_LENGTH = 4;
- public static final int STORAGE_PERMISSION = 0,
- INSTALL_APK_PERMISSION = 1,
- ALL_FILES_PERMISSION = 2,
- NOTIFICATION_PERMISSION = 3;
-
- private final OnPermissionGranted[] permissionCallbacks =
- new OnPermissionGranted[PERMISSION_LENGTH];
-
- @Override
- public void onRequestPermissionsResult(
- int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (requestCode == STORAGE_PERMISSION) {
- if (isGranted(grantResults)) {
- Utils.enableScreenRotation(this);
- permissionCallbacks[STORAGE_PERMISSION].onPermissionGranted();
- permissionCallbacks[STORAGE_PERMISSION] = null;
- } else {
- Toast.makeText(this, R.string.grantfailed, Toast.LENGTH_SHORT).show();
- requestStoragePermission(permissionCallbacks[STORAGE_PERMISSION], false);
- }
- } else if (requestCode == NOTIFICATION_PERMISSION && SDK_INT >= TIRAMISU) {
- if (isGranted(grantResults)) {
- Utils.enableScreenRotation(this);
- } else {
- Toast.makeText(this, R.string.grantfailed, Toast.LENGTH_SHORT).show();
- requestNotificationPermission(false);
- }
- } else if (requestCode == INSTALL_APK_PERMISSION) {
- if (isGranted(grantResults)) {
- permissionCallbacks[INSTALL_APK_PERMISSION].onPermissionGranted();
- permissionCallbacks[INSTALL_APK_PERMISSION] = null;
- }
- }
- }
-
- public boolean checkStoragePermission() {
- // Verify that all required contact permissions have been granted.
- if (SDK_INT >= Build.VERSION_CODES.R) {
- return (ActivityCompat.checkSelfPermission(
- this, Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
- == PackageManager.PERMISSION_GRANTED)
- || (ActivityCompat.checkSelfPermission(
- this, Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
- == PackageManager.PERMISSION_GRANTED)
- || Environment.isExternalStorageManager();
- } else {
- return ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
- == PackageManager.PERMISSION_GRANTED;
- }
- }
-
- @RequiresApi(TIRAMISU)
- public boolean checkNotificationPermission() {
- return ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
- == PackageManager.PERMISSION_GRANTED;
- }
-
- @RequiresApi(TIRAMISU)
- public void requestNotificationPermission(boolean isInitialStart) {
- Utils.disableScreenRotation(this);
- final MaterialDialog materialDialog =
- GeneralDialogCreation.showBasicDialog(
- this,
- R.string.grant_notification_permission,
- R.string.grantper,
- R.string.grant,
- R.string.cancel);
- materialDialog.getActionButton(DialogAction.NEGATIVE).setOnClickListener(v -> finish());
- materialDialog.setCancelable(false);
-
- requestPermission(
- Manifest.permission.POST_NOTIFICATIONS,
- NOTIFICATION_PERMISSION,
- materialDialog,
- () -> {
- // do nothing
- },
- isInitialStart);
- }
-
- public void requestStoragePermission(
- @NonNull final OnPermissionGranted onPermissionGranted, boolean isInitialStart) {
- Utils.disableScreenRotation(this);
- final MaterialDialog materialDialog =
- GeneralDialogCreation.showBasicDialog(
- this,
- R.string.grant_storage_permission,
- R.string.grantper,
- R.string.grant,
- R.string.cancel);
- materialDialog.getActionButton(DialogAction.NEGATIVE).setOnClickListener(v -> finish());
- materialDialog.setCancelable(false);
-
- requestPermission(
- Manifest.permission.WRITE_EXTERNAL_STORAGE,
- STORAGE_PERMISSION,
- materialDialog,
- onPermissionGranted,
- isInitialStart);
- }
-
- @RequiresApi(api = Build.VERSION_CODES.M)
- public void requestInstallApkPermission(
- @NonNull final OnPermissionGranted onPermissionGranted, boolean isInitialStart) {
- final MaterialDialog materialDialog =
- GeneralDialogCreation.showBasicDialog(
- this,
- R.string.grant_apkinstall_permission,
- R.string.grantper,
- R.string.grant,
- R.string.cancel);
- materialDialog
- .getActionButton(DialogAction.NEGATIVE)
- .setOnClickListener(v -> materialDialog.dismiss());
- materialDialog.setCancelable(false);
-
- requestPermission(
- Manifest.permission.REQUEST_INSTALL_PACKAGES,
- INSTALL_APK_PERMISSION,
- materialDialog,
- onPermissionGranted,
- isInitialStart);
- }
-
- /**
- * Requests permission, overrides {@param rationale}'s POSITIVE button dialog action.
- *
- * @param permission The permission to ask for
- * @param code {@link #STORAGE_PERMISSION} or {@link #INSTALL_APK_PERMISSION}
- * @param rationale MaterialLayout to provide an additional rationale to the user if the
- * permission was not granted and the user would benefit from additional context for the use
- * of the permission. For example, if the request has been denied previously.
- * @param isInitialStart is the permission being requested for the first time in the application
- * lifecycle
- */
- private void requestPermission(
- final String permission,
- final int code,
- @NonNull final MaterialDialog rationale,
- @NonNull final OnPermissionGranted onPermissionGranted,
- boolean isInitialStart) {
- permissionCallbacks[code] = onPermissionGranted;
-
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
- rationale
- .getActionButton(DialogAction.POSITIVE)
- .setOnClickListener(
- v -> {
- ActivityCompat.requestPermissions(
- PermissionsActivity.this, new String[] {permission}, code);
- rationale.dismiss();
- });
- rationale.show();
- } else if (isInitialStart) {
- ActivityCompat.requestPermissions(this, new String[] {permission}, code);
- } else {
- if (SDK_INT >= Build.VERSION_CODES.R) {
- Snackbar.make(
- findViewById(R.id.content_frame),
- R.string.grantfailed,
- BaseTransientBottomBar.LENGTH_INDEFINITE)
- .setAction(R.string.grant, v -> requestAllFilesAccessPermission(onPermissionGranted))
- .show();
- } else {
- Snackbar.make(
- findViewById(R.id.content_frame),
- R.string.grantfailed,
- BaseTransientBottomBar.LENGTH_INDEFINITE)
- .setAction(
- R.string.grant,
- v ->
- startActivity(
- new Intent(
- android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
- Uri.parse(String.format("package:%s", getPackageName())))))
- .show();
- }
- }
- }
-
- /**
- * Request all files access on android 11+
- *
- * @param onPermissionGranted permission granted callback
- */
- public void requestAllFilesAccess(@NonNull final OnPermissionGranted onPermissionGranted) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
- final MaterialDialog materialDialog =
- GeneralDialogCreation.showBasicDialog(
- this,
- R.string.grant_all_files_permission,
- R.string.grantper,
- R.string.grant,
- R.string.cancel);
- materialDialog.getActionButton(DialogAction.NEGATIVE).setOnClickListener(v -> finish());
- materialDialog
- .getActionButton(DialogAction.POSITIVE)
- .setOnClickListener(
- v -> {
- requestAllFilesAccessPermission(onPermissionGranted);
- materialDialog.dismiss();
- });
- materialDialog.setCancelable(false);
- materialDialog.show();
- }
- }
-
- @RequiresApi(api = Build.VERSION_CODES.R)
- private void requestAllFilesAccessPermission(
- @NonNull final OnPermissionGranted onPermissionGranted) {
- Utils.disableScreenRotation(this);
- permissionCallbacks[ALL_FILES_PERMISSION] = onPermissionGranted;
- try {
- Intent intent =
- new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
- .setData(Uri.parse("package:" + getPackageName()));
- startActivity(intent);
- } catch (ActivityNotFoundException anf) {
- // fallback
- try {
- Intent intent =
- new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
- .setData(Uri.parse("package:$packageName"));
- startActivity(intent);
- } catch (Exception e) {
- AppConfig.toast(this, getString(R.string.grantfailed));
- }
- } catch (Exception e) {
- Log.e(TAG, "Failed to initial activity to grant all files access", e);
- AppConfig.toast(this, getString(R.string.grantfailed));
- }
- }
-
- private boolean isGranted(int[] grantResults) {
- return grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
- }
-
- public interface OnPermissionGranted {
- void onPermissionGranted();
- }
-}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.kt
new file mode 100644
index 0000000000..6f2cdd608e
--- /dev/null
+++ b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.kt
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
+ * Emmanuel Messulam, Raymond Lai and Contributors.
+ *
+ * This file is part of Amaze File Manager.
+ *
+ * Amaze File Manager is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.amaze.filemanager.ui.activities.superclasses
+
+import android.Manifest
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build.VERSION
+import android.os.Build.VERSION_CODES
+import android.os.Environment
+import android.provider.Settings
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.afollestad.materialdialogs.DialogAction
+import com.afollestad.materialdialogs.MaterialDialog
+import com.amaze.filemanager.R
+import com.amaze.filemanager.application.AppConfig
+import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation
+import com.amaze.filemanager.utils.Utils
+import com.google.android.material.snackbar.BaseTransientBottomBar
+import com.google.android.material.snackbar.Snackbar
+
+open class PermissionsActivity :
+ ThemedActivity(),
+ ActivityCompat.OnRequestPermissionsResultCallback {
+ private val permissionCallbacks: Array<(() -> Unit)?> = arrayOfNulls(PERMISSION_LENGTH)
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray,
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == STORAGE_PERMISSION) {
+ if (isGranted(grantResults)) {
+ Utils.enableScreenRotation(this)
+ permissionCallbacks[STORAGE_PERMISSION]?.invoke()
+ permissionCallbacks[STORAGE_PERMISSION] = null
+ } else {
+ Toast.makeText(this, R.string.grantfailed, Toast.LENGTH_SHORT).show()
+ requestStoragePermission(permissionCallbacks[STORAGE_PERMISSION]!!, false)
+ }
+ } else if (requestCode == NOTIFICATION_PERMISSION && VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
+ if (isGranted(grantResults)) {
+ Utils.enableScreenRotation(this)
+ } else {
+ Toast.makeText(this, R.string.grantfailed, Toast.LENGTH_SHORT).show()
+ requestNotificationPermission(false)
+ }
+ } else if (requestCode == INSTALL_APK_PERMISSION) {
+ if (isGranted(grantResults)) {
+ permissionCallbacks[INSTALL_APK_PERMISSION]?.invoke()
+ permissionCallbacks[INSTALL_APK_PERMISSION] = null
+ }
+ }
+ }
+
+ /**
+ * Check and prompt user to grant storage permission.
+ */
+ fun checkStoragePermission(): Boolean {
+ // Verify that all required contact permissions have been granted.
+ return if (VERSION.SDK_INT >= VERSION_CODES.R) {
+ (
+ (
+ ActivityCompat.checkSelfPermission(
+ this,
+ Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
+ )
+ == PackageManager.PERMISSION_GRANTED
+ ) ||
+ (
+ ActivityCompat.checkSelfPermission(
+ this,
+ Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION,
+ )
+ == PackageManager.PERMISSION_GRANTED
+ ) ||
+ Environment.isExternalStorageManager()
+ )
+ } else {
+ (
+ ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ )
+ == PackageManager.PERMISSION_GRANTED
+ )
+ }
+ }
+
+ /**
+ * Check and prompt user to grant notification permission. For Android >= 8.
+ */
+ @RequiresApi(VERSION_CODES.TIRAMISU)
+ fun checkNotificationPermission(): Boolean {
+ return (
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ == PackageManager.PERMISSION_GRANTED
+ )
+ }
+
+ /**
+ * Request notification permission.
+ */
+ @RequiresApi(VERSION_CODES.TIRAMISU)
+ fun requestNotificationPermission(isInitialStart: Boolean) {
+ Utils.disableScreenRotation(this)
+ val materialDialog =
+ GeneralDialogCreation.showBasicDialog(
+ this,
+ R.string.grant_notification_permission,
+ R.string.grantper,
+ R.string.grant,
+ R.string.cancel,
+ )
+ materialDialog.getActionButton(DialogAction.NEGATIVE)
+ .setOnClickListener { v: View? -> finish() }
+ materialDialog.setCancelable(false)
+
+ requestPermission(
+ Manifest.permission.POST_NOTIFICATIONS,
+ NOTIFICATION_PERMISSION,
+ materialDialog,
+ { },
+ isInitialStart,
+ )
+ }
+
+ /**
+ * Request storage permission.
+ */
+ fun requestStoragePermission(
+ onPermissionGranted: (() -> Unit),
+ isInitialStart: Boolean,
+ ) {
+ Utils.disableScreenRotation(this)
+ val materialDialog =
+ GeneralDialogCreation.showBasicDialog(
+ this,
+ R.string.grant_storage_permission,
+ R.string.grantper,
+ R.string.grant,
+ R.string.cancel,
+ )
+ materialDialog.getActionButton(DialogAction.NEGATIVE)
+ .setOnClickListener { v: View? -> finish() }
+ materialDialog.setCancelable(false)
+
+ requestPermission(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ STORAGE_PERMISSION,
+ materialDialog,
+ onPermissionGranted,
+ isInitialStart,
+ )
+ }
+
+ /**
+ * Request install app permission. For Android >= 6.
+ */
+ @RequiresApi(api = VERSION_CODES.M)
+ fun requestInstallApkPermission(
+ onPermissionGranted: (() -> Unit),
+ isInitialStart: Boolean,
+ ) {
+ val materialDialog =
+ GeneralDialogCreation.showBasicDialog(
+ this,
+ R.string.grant_apkinstall_permission,
+ R.string.grantper,
+ R.string.grant,
+ R.string.cancel,
+ )
+ materialDialog
+ .getActionButton(DialogAction.NEGATIVE)
+ .setOnClickListener { v: View? -> materialDialog.dismiss() }
+ materialDialog.setCancelable(false)
+
+ requestPermission(
+ Manifest.permission.REQUEST_INSTALL_PACKAGES,
+ INSTALL_APK_PERMISSION,
+ materialDialog,
+ onPermissionGranted,
+ isInitialStart,
+ )
+ }
+
+ /**
+ * Request terminal app permission. Probably dialog won't popup as it's 3rd party permissions,
+ * but does prompt user to grant if not granted yet.
+ */
+ fun requestTerminalPermission(
+ permission: String,
+ onPermissionGranted: (() -> Unit),
+ ) {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ permission,
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ onPermissionGranted.invoke()
+ } else {
+ val materialDialog =
+ GeneralDialogCreation.showBasicDialog(
+ this,
+ R.string.grant_terminal_permission,
+ R.string.grantper,
+ R.string.grant,
+ R.string.cancel,
+ )
+ materialDialog
+ .getActionButton(DialogAction.NEGATIVE)
+ .setOnClickListener { v: View? -> materialDialog.dismiss() }
+ materialDialog.setCancelable(false)
+ requestPermission(
+ permission,
+ TERMINAL_PERMISSION,
+ materialDialog,
+ onPermissionGranted,
+ false,
+ )
+ }
+ }
+
+ /**
+ * Requests permission, overrides {@param rationale}'s POSITIVE button dialog action.
+ *
+ * @param permission The permission to ask for
+ * @param code [.STORAGE_PERMISSION] or [.INSTALL_APK_PERMISSION]
+ * @param rationale MaterialLayout to provide an additional rationale to the user if the
+ * permission was not granted and the user would benefit from additional context for the use
+ * of the permission. For example, if the request has been denied previously.
+ * @param isInitialStart is the permission being requested for the first time in the application
+ * lifecycle
+ */
+ private fun requestPermission(
+ permission: String,
+ code: Int,
+ rationale: MaterialDialog,
+ onPermissionGranted: (() -> Unit),
+ isInitialStart: Boolean,
+ ) {
+ permissionCallbacks[code] = onPermissionGranted
+
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
+ rationale
+ .getActionButton(DialogAction.POSITIVE)
+ .setOnClickListener { v: View? ->
+ ActivityCompat.requestPermissions(
+ this@PermissionsActivity,
+ arrayOf(permission),
+ code,
+ )
+ rationale.dismiss()
+ }
+ rationale.show()
+ } else if (isInitialStart) {
+ ActivityCompat.requestPermissions(this, arrayOf(permission), code)
+ } else {
+ if (VERSION.SDK_INT >= VERSION_CODES.R) {
+ Snackbar.make(
+ findViewById(R.id.content_frame),
+ R.string.grantfailed,
+ BaseTransientBottomBar.LENGTH_INDEFINITE,
+ )
+ .setAction(R.string.grant) { v: View? ->
+ requestAllFilesAccessPermission(
+ onPermissionGranted,
+ )
+ }
+ .show()
+ } else {
+ Snackbar.make(
+ findViewById(R.id.content_frame),
+ R.string.grantfailed,
+ BaseTransientBottomBar.LENGTH_INDEFINITE,
+ )
+ .setAction(
+ R.string.grant,
+ ) { v: View? ->
+ startActivity(
+ Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.parse(String.format("package:%s", packageName)),
+ ),
+ )
+ }
+ .show()
+ }
+ }
+ }
+
+ /**
+ * Request all files access on android 11+
+ *
+ * @param onPermissionGranted permission granted callback
+ */
+ fun requestAllFilesAccess(onPermissionGranted: (() -> Unit)) {
+ if (VERSION.SDK_INT >= VERSION_CODES.R && !Environment.isExternalStorageManager()) {
+ val materialDialog =
+ GeneralDialogCreation.showBasicDialog(
+ this,
+ R.string.grant_all_files_permission,
+ R.string.grantper,
+ R.string.grant,
+ R.string.cancel,
+ )
+ materialDialog.getActionButton(DialogAction.NEGATIVE)
+ .setOnClickListener { v: View? -> finish() }
+ materialDialog
+ .getActionButton(DialogAction.POSITIVE)
+ .setOnClickListener { v: View? ->
+ requestAllFilesAccessPermission(onPermissionGranted)
+ materialDialog.dismiss()
+ }
+ materialDialog.setCancelable(false)
+ materialDialog.show()
+ }
+ }
+
+ @RequiresApi(api = VERSION_CODES.R)
+ private fun requestAllFilesAccessPermission(onPermissionGranted: (() -> Unit)) {
+ Utils.disableScreenRotation(this)
+ permissionCallbacks[ALL_FILES_PERMISSION] = onPermissionGranted
+ try {
+ val intent =
+ Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
+ .setData(Uri.parse("package:$packageName"))
+ startActivity(intent)
+ } catch (anf: ActivityNotFoundException) {
+ // fallback
+ try {
+ val intent =
+ Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
+ .setData(Uri.parse("package:\$packageName"))
+ startActivity(intent)
+ } catch (e: Exception) {
+ AppConfig.toast(this, getString(R.string.grantfailed))
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to initial activity to grant all files access", e)
+ AppConfig.toast(this, getString(R.string.grantfailed))
+ }
+ }
+
+ private fun isGranted(grantResults: IntArray): Boolean {
+ return grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED
+ }
+
+ companion object {
+ private val TAG: String = PermissionsActivity::class.java.simpleName
+
+ const val PERMISSION_LENGTH: Int = 5
+ const val STORAGE_PERMISSION: Int = 0
+ const val INSTALL_APK_PERMISSION: Int = 1
+ const val ALL_FILES_PERMISSION: Int = 2
+ const val NOTIFICATION_PERMISSION: Int = 3
+ const val TERMINAL_PERMISSION: Int = 4
+ }
+}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PreferenceActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PreferenceActivity.java
deleted file mode 100644
index 526264186f..0000000000
--- a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PreferenceActivity.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
- * Emmanuel Messulam, Raymond Lai and Contributors.
- *
- * This file is part of Amaze File Manager.
- *
- * Amaze File Manager is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.amaze.filemanager.ui.activities.superclasses;
-
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_BOOKMARKS_ADDED;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_CHANGEPATHS;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_COLORED_NAVIGATION;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_COLORIZE_ICONS;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_DISABLE_PLAYER_INTENT_FILTERS;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_ENABLE_MARQUEE_FILENAME;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_NEED_TO_SET_HOME;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_ROOTMODE;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_ROOT_LEGACY_LISTING;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_DIVIDERS;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_FILE_SIZE;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_GOBACK_BUTTON;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HEADERS;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_LAST_MODIFIED;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_PERMISSIONS;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_SIDEBAR_FOLDERS;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_SIDEBAR_QUICKACCESSES;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_THUMB;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_TEXTEDITOR_NEWSTACK;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_USE_CIRCULAR_IMAGES;
-import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_VIEW;
-
-import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants;
-import com.amaze.filemanager.utils.PreferenceUtils;
-
-import android.content.SharedPreferences;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.preference.PreferenceManager;
-
-/**
- * @author Emmanuel on 24/8/2017, at 23:13.
- */
-public class PreferenceActivity extends BasicActivity {
-
- private SharedPreferences sharedPrefs;
-
- @Override
- public void onCreate(final Bundle savedInstanceState) {
- // Fragments are created before the super call returns, so we must
- // initialize sharedPrefs before the super call otherwise it cannot be used by fragments
- sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
-
- super.onCreate(savedInstanceState);
- }
-
- @NonNull
- public SharedPreferences getPrefs() {
- return sharedPrefs;
- }
-
- public boolean isRootExplorer() {
- return getBoolean(PREFERENCE_ROOTMODE);
- }
-
- public int getCurrentTab() {
- return getPrefs()
- .getInt(PreferencesConstants.PREFERENCE_CURRENT_TAB, PreferenceUtils.DEFAULT_CURRENT_TAB);
- }
-
- public boolean getBoolean(String key) {
- boolean defaultValue;
-
- switch (key) {
- case PREFERENCE_SHOW_PERMISSIONS:
- case PREFERENCE_SHOW_GOBACK_BUTTON:
- case PREFERENCE_SHOW_HIDDENFILES:
- case PREFERENCE_BOOKMARKS_ADDED:
- case PREFERENCE_ROOTMODE:
- case PREFERENCE_COLORED_NAVIGATION:
- case PREFERENCE_TEXTEDITOR_NEWSTACK:
- case PREFERENCE_CHANGEPATHS:
- case PREFERENCE_ROOT_LEGACY_LISTING:
- case PREFERENCE_DISABLE_PLAYER_INTENT_FILTERS:
- defaultValue = false;
- break;
- case PREFERENCE_SHOW_FILE_SIZE:
- case PREFERENCE_SHOW_DIVIDERS:
- case PREFERENCE_SHOW_HEADERS:
- case PREFERENCE_USE_CIRCULAR_IMAGES:
- case PREFERENCE_COLORIZE_ICONS:
- case PREFERENCE_SHOW_THUMB:
- case PREFERENCE_SHOW_SIDEBAR_QUICKACCESSES:
- case PREFERENCE_NEED_TO_SET_HOME:
- case PREFERENCE_SHOW_SIDEBAR_FOLDERS:
- case PREFERENCE_VIEW:
- case PREFERENCE_SHOW_LAST_MODIFIED:
- case PREFERENCE_ENABLE_MARQUEE_FILENAME:
- defaultValue = true;
- break;
- default:
- throw new IllegalArgumentException("Please map \'" + key + "\'");
- }
-
- return sharedPrefs.getBoolean(key, defaultValue);
- }
-}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PreferenceActivity.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PreferenceActivity.kt
new file mode 100644
index 0000000000..210ecf68ee
--- /dev/null
+++ b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PreferenceActivity.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
+ * Emmanuel Messulam, Raymond Lai and Contributors.
+ *
+ * This file is part of Amaze File Manager.
+ *
+ * Amaze File Manager is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.amaze.filemanager.ui.activities.superclasses
+
+import android.content.SharedPreferences
+import android.os.Bundle
+import androidx.preference.PreferenceManager
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_BOOKMARKS_ADDED
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_CHANGEPATHS
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_COLORED_NAVIGATION
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_COLORIZE_ICONS
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_DISABLE_PLAYER_INTENT_FILTERS
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_ENABLE_MARQUEE_FILENAME
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_NEED_TO_SET_HOME
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_ROOTMODE
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_ROOT_LEGACY_LISTING
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_DIVIDERS
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_FILE_SIZE
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_GOBACK_BUTTON
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HEADERS
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_LAST_MODIFIED
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_PERMISSIONS
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_SIDEBAR_FOLDERS
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_SIDEBAR_QUICKACCESSES
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_THUMB
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_TEXTEDITOR_NEWSTACK
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_USE_CIRCULAR_IMAGES
+import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_VIEW
+import com.amaze.filemanager.utils.PreferenceUtils
+
+/**
+ * @author Emmanuel on 24/8/2017, at 23:13.
+ */
+open class PreferenceActivity : BasicActivity() {
+ private var sharedPrefs: SharedPreferences? = null
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ // Fragments are created before the super call returns, so we must
+ // initialize sharedPrefs before the super call otherwise it cannot be used by fragments
+ sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
+ super.onCreate(savedInstanceState)
+ }
+
+ val prefs: SharedPreferences
+ get() = sharedPrefs!!
+
+ val isRootExplorer: Boolean
+ get() = getBoolean(PREFERENCE_ROOTMODE)
+
+ val currentTab: Int
+ get() =
+ prefs
+ .getInt(
+ PreferencesConstants.PREFERENCE_CURRENT_TAB,
+ PreferenceUtils.DEFAULT_CURRENT_TAB,
+ )
+
+ /**
+ * Convenience method to [SharedPreferences.getBoolean] for quickly getting user preference flags.
+ */
+ fun getBoolean(key: String): Boolean {
+ val defaultValue =
+ when (key) {
+ PREFERENCE_SHOW_PERMISSIONS,
+ PREFERENCE_SHOW_GOBACK_BUTTON,
+ PREFERENCE_SHOW_HIDDENFILES,
+ PREFERENCE_BOOKMARKS_ADDED,
+ PREFERENCE_ROOTMODE,
+ PREFERENCE_COLORED_NAVIGATION,
+ PREFERENCE_TEXTEDITOR_NEWSTACK,
+ PREFERENCE_CHANGEPATHS,
+ PREFERENCE_ROOT_LEGACY_LISTING,
+ PREFERENCE_DISABLE_PLAYER_INTENT_FILTERS,
+ -> false
+ PREFERENCE_SHOW_FILE_SIZE,
+ PREFERENCE_SHOW_DIVIDERS,
+ PREFERENCE_SHOW_HEADERS,
+ PREFERENCE_USE_CIRCULAR_IMAGES,
+ PREFERENCE_COLORIZE_ICONS,
+ PREFERENCE_SHOW_THUMB,
+ PREFERENCE_SHOW_SIDEBAR_QUICKACCESSES,
+ PREFERENCE_NEED_TO_SET_HOME,
+ PREFERENCE_SHOW_SIDEBAR_FOLDERS,
+ PREFERENCE_VIEW,
+ PREFERENCE_SHOW_LAST_MODIFIED,
+ PREFERENCE_ENABLE_MARQUEE_FILENAME,
+ -> true
+ else -> throw IllegalArgumentException("Please map '$key'")
+ }
+ return sharedPrefs!!.getBoolean(key, defaultValue)
+ }
+}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/OpenFileDialogFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/OpenFileDialogFragment.kt
index 72a063dbb6..5ae7766c1c 100644
--- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/OpenFileDialogFragment.kt
+++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/OpenFileDialogFragment.kt
@@ -54,6 +54,7 @@ import com.amaze.filemanager.ui.provider.UtilitiesProvider
import com.amaze.filemanager.ui.startActivityCatchingSecurityException
import com.amaze.filemanager.ui.views.ThemedTextView
import com.amaze.filemanager.utils.GlideConstants
+import com.amaze.filemanager.utils.queryIntentActivitiesCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.util.ViewPreloadSizeProvider
@@ -159,7 +160,7 @@ class OpenFileDialogFragment : BaseBottomSheetFragment(), AdjustListViewForTv {
val packageManager = requireContext().packageManager
val appDataParcelableList: MutableList = ArrayList()
- packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL).forEach {
+ packageManager.queryIntentActivitiesCompat(intent, PackageManager.MATCH_ALL).forEach {
val openFileParcelable =
OpenFileParcelable(
uri,
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..0b08be345b
--- /dev/null
+++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/OpenFolderInTerminalFragment.kt
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
+ * Emmanuel Messulam, Raymond Lai and Contributors.
+ *
+ * This file is part of Amaze File Manager.
+ *
+ * Amaze File Manager is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.amaze.filemanager.ui.dialogs
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+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.PermissionsActivity
+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.ANDROID_TERM
+import com.amaze.filemanager.utils.GlideConstants
+import com.amaze.filemanager.utils.TERMONE_PLUS
+import com.amaze.filemanager.utils.TERMUX
+import com.amaze.filemanager.utils.detectInstalledTerminalApps
+import com.amaze.filemanager.utils.getApplicationInfoCompat
+import com.amaze.filemanager.utils.getPackageInfoCompat
+import com.bumptech.glide.Glide
+import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
+import com.bumptech.glide.util.ViewPreloadSizeProvider
+import org.slf4j.LoggerFactory
+
+/**
+ * Bottom sheet fragment for open folder in terminal app actions.
+ *
+ * Supports Termux and Termone plus (and possibly its predecessor, Jack Palovich's terminal app).
+ */
+class OpenFolderInTerminalFragment : BaseBottomSheetFragment(), AdjustListViewForTv {
+ private var fragmentOpenFileDialogBinding: FragmentOpenFileDialogBinding? = null
+
+ @VisibleForTesting
+ internal 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)
+
+ const val KEY_PREFERENCES_DEFAULT = "terminal._DEFAULT"
+ const val KEY_PREFERENCES_LAST = "terminal._LAST"
+
+ private const val TERMONE_PLUS_PERMISSION = "com.termoneplus.permission.RUN_SCRIPT"
+ private const val ANDROID_TERM_PERMISSION = "jackpal.androidterm.permission.RUN_SCRIPT"
+ private const val TERMUX_PERMISSION = "com.termux.permission.RUN_COMMAND"
+
+ @SuppressLint("SdCardPath")
+ private const val TERMUX_SHELL_LOCATION = "/data/data/com.termux/files/usr/bin/bash"
+
+ /**
+ * Public facing method. Opens this sheet fragment for user to choose the terminal app.
+ *
+ * Supports Termux, Jack Palovich's terminal app and Termone plus.
+ */
+ 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) {
+ startActivity(activity, buildIntent(installedTerminals.first(), path))
+ } else {
+ val packageName = activity.prefs.getString(KEY_PREFERENCES_DEFAULT, null)
+ if (true == packageName?.isNotEmpty()) {
+ startActivity(activity, buildIntent(packageName, path))
+ } else {
+ newInstance(path, installedTerminals).show(
+ activity.supportFragmentManager,
+ OpenFolderInTerminalFragment::class.java.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
+ }
+
+ private fun startActivity(
+ context: PermissionsActivity,
+ intent: Intent,
+ ) {
+ if (TERMUX == intent.component?.packageName) {
+ context.requestTerminalPermission(TERMUX_PERMISSION) {
+ ContextCompat.startForegroundService(context, intent)
+ }
+ } else if (TERMONE_PLUS == intent.component?.packageName) {
+ context.requestTerminalPermission(TERMONE_PLUS_PERMISSION) {
+ ContextCompat.startActivity(context, intent, null)
+ }
+ } else if (ANDROID_TERM == intent.component?.packageName) {
+ context.requestTerminalPermission(ANDROID_TERM_PERMISSION) {
+ ContextCompat.startActivity(context, intent, null)
+ }
+ } else {
+ logger.error(
+ "Invalid intent - intent.component is null or package name supported: ${intent.component?.packageName}",
+ )
+ }
+ }
+
+ private fun buildIntent(
+ packageName: String,
+ path: String,
+ ): Intent {
+ return when (packageName) {
+ TERMONE_PLUS -> {
+ Intent().also {
+ it.action = "$TERMONE_PLUS.RUN_SCRIPT"
+ it.setClassName(TERMONE_PLUS, "$ANDROID_TERM.RunScript")
+ it.putExtra("$TERMONE_PLUS.Command", "cd \"$path\"")
+ }
+ }
+
+ ANDROID_TERM -> {
+ Intent().also {
+ it.action = "$ANDROID_TERM.RUN_SCRIPT"
+ it.setClassName(ANDROID_TERM, "$ANDROID_TERM.RunScript")
+ it.putExtra("$ANDROID_TERM.iInitialCommand", "cd \"$path\"")
+ }
+ }
+
+ TERMUX -> {
+ Intent().also {
+ it.setClassName(TERMUX, "$TERMUX.app.RunCommandService")
+ it.setAction("$TERMUX.RUN_COMMAND")
+ it.putExtra(
+ "$TERMUX.RUN_COMMAND_PATH",
+ TERMUX_SHELL_LOCATION,
+ )
+ it.putExtra("$TERMUX.RUN_COMMAND_WORKDIR", path)
+ }
+ }
+ else -> throw IllegalArgumentException("Unsupported package: $packageName")
+ }
+ }
+
+ /**
+ * 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,
+ sharedPreferences: SharedPreferences,
+ ) {
+ sharedPreferences.edit().putString(
+ KEY_PREFERENCES_LAST,
+ appDataParcelable.packageName,
+ ).apply()
+ }
+
+ /**
+ * Sets default app for mime type selected using 'Always' button from bottom sheet
+ */
+ private fun setDefaultOpenedApp(
+ appDataParcelable: AppDataParcelable,
+ sharedPreferences: SharedPreferences,
+ ) {
+ sharedPreferences.edit().putString(
+ KEY_PREFERENCES_DEFAULT,
+ appDataParcelable.packageName,
+ ).apply()
+ }
+
+ /**
+ * Clears all default apps set preferences for mime types
+ */
+ fun clearPreferences(sharedPreferences: SharedPreferences) {
+ AppConfig.getInstance().runInBackground {
+ arrayOf(KEY_PREFERENCES_DEFAULT, KEY_PREFERENCES_LAST).forEach {
+ sharedPreferences.edit().remove(it).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_TERMINAL_APPS,
+ )
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
+
+ val appDataParcelableList = initList()
+ val lastClassAndPackage =
+ sharedPreferences
+ .getString(KEY_PREFERENCES_LAST, null)
+ val lastAppData: AppDataParcelable =
+ initLastAppData(
+ lastClassAndPackage,
+ appDataParcelableList,
+ ) ?: return
+
+ adapter =
+ AppsRecyclerAdapter(
+ this,
+ modelProvider,
+ true,
+ this,
+ appDataParcelableList,
+ ) { rowItem ->
+ setLastOpenedApp(rowItem, sharedPreferences)
+ startActivity(
+ requireActivity() as PermissionsActivity,
+ buildIntent(rowItem.packageName, path),
+ )
+ dismiss()
+ }
+ 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.getPackageInfoCompat(pkg, 0)
+ }.onFailure {
+ logger.error("Error getting package info for $pkg", it)
+ }.getOrNull()?.run {
+ packageManager.getApplicationInfoCompat(pkg, 0).let { applicationInfo ->
+ appDataParcelableList.add(
+ AppDataParcelable(
+ packageManager.getApplicationLabel(applicationInfo).toString(),
+ "",
+ null,
+ this.packageName,
+ "",
+ "",
+ 0,
+ 0, false,
+ null,
+ ),
+ )
+ }
+ }
+ }
+ return appDataParcelableList
+ }
+
+ private fun initLastAppData(
+ lastClassAndPackage: String?,
+ appDataParcelableList: MutableList,
+ ): AppDataParcelable? {
+ if (appDataParcelableList.size == 0) {
+ AppConfig.toast(requireContext(), "No terminal apps available")
+ dismiss()
+ return null
+ }
+
+ if (appDataParcelableList.size == 1) {
+ startActivity(buildIntent(appDataParcelableList.first().packageName, path))
+ }
+
+ var lastAppData: AppDataParcelable? =
+ if (!lastClassAndPackage.isNullOrEmpty()) {
+ appDataParcelableList.find {
+ it.packageName == lastClassAndPackage
+ }
+ } else {
+ null
+ }
+ lastAppData = lastAppData ?: appDataParcelableList[0]
+ appDataParcelableList.remove(lastAppData)
+ return lastAppData
+ }
+
+ private fun loadViews(lastAppData: AppDataParcelable) {
+ lastAppData.let {
+ val lastAppIntent = buildIntent(lastAppData.packageName, path)
+
+ viewBinding.run {
+ appsRecyclerView.layoutManager = LinearLayoutManager(requireContext())
+ appsRecyclerView.adapter = adapter
+
+ lastAppTitle.text = it.label
+ lastAppImage.setImageDrawable(
+ requireActivity().packageManager.getApplicationIcon(it.packageName),
+ )
+
+ justOnceButton.setTextColor((activity as ThemedActivity).accent)
+ justOnceButton.setOnClickListener { _ ->
+ setLastOpenedApp(it, sharedPreferences)
+ startActivity(requireActivity() as PermissionsActivity, lastAppIntent)
+ dismiss()
+ }
+ alwaysButton.setTextColor((activity as ThemedActivity).accent)
+ alwaysButton.setOnClickListener { _ ->
+ setDefaultOpenedApp(it, sharedPreferences)
+ startActivity(requireActivity() as PermissionsActivity, lastAppIntent)
+ dismiss()
+ }
+ }
+ }
+ }
+
+ override fun adjustListViewForTv(
+ viewHolder: AppHolder,
+ mainActivity: MainActivity,
+ ) {
+ // do nothing
+ }
+}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/AppsListFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/AppsListFragment.java
index fd8355f064..508e52c337 100644
--- a/app/src/main/java/com/amaze/filemanager/ui/fragments/AppsListFragment.java
+++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/AppsListFragment.java
@@ -263,7 +263,7 @@ public void onLoadFinished(
}
adapterList.add(appDataParcelable);
}
- adapter = new AppsRecyclerAdapter(this, modelProvider, false, this, adapterList);
+ adapter = new AppsRecyclerAdapter(this, modelProvider, false, this, adapterList, null);
getRecyclerView().setVisibility(View.VISIBLE);
getRecyclerView().setAdapter(adapter);
}
diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BehaviorPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BehaviorPrefsFragment.kt
index 189b77b104..e277a13765 100644
--- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BehaviorPrefsFragment.kt
+++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BehaviorPrefsFragment.kt
@@ -29,7 +29,8 @@ import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.folderselector.FolderChooserDialog
import com.amaze.filemanager.R
import com.amaze.filemanager.application.AppConfig
-import com.amaze.filemanager.ui.dialogs.OpenFileDialogFragment.Companion.clearPreferences
+import com.amaze.filemanager.ui.dialogs.OpenFileDialogFragment
+import com.amaze.filemanager.ui.dialogs.OpenFolderInTerminalFragment
import com.amaze.trashbin.TrashBinConfig
import java.io.File
@@ -44,7 +45,8 @@ class BehaviorPrefsFragment : BasePrefsFragment(), FolderChooserDialog.FolderCal
findPreference("clear_open_file")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
- clearPreferences(activity.prefs)
+ OpenFileDialogFragment.clearPreferences(activity.prefs)
+ OpenFolderInTerminalFragment.clearPreferences(activity.prefs)
AppConfig.toast(getActivity(), activity.getString(R.string.done))
true
}
diff --git a/app/src/main/java/com/amaze/filemanager/utils/GlideConstants.java b/app/src/main/java/com/amaze/filemanager/utils/GlideConstants.kt
similarity index 83%
rename from app/src/main/java/com/amaze/filemanager/utils/GlideConstants.java
rename to app/src/main/java/com/amaze/filemanager/utils/GlideConstants.kt
index c8ec849dbc..e1a0a9c252 100644
--- a/app/src/main/java/com/amaze/filemanager/utils/GlideConstants.java
+++ b/app/src/main/java/com/amaze/filemanager/utils/GlideConstants.kt
@@ -17,14 +17,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
-package com.amaze.filemanager.utils;
+package com.amaze.filemanager.utils
/**
* @author Emmanuel Messulam on 8/12/2017, at 16:33.
*/
-public class GlideConstants {
-
- public static final int MAX_PRELOAD_FILES = 50;
- public static final int MAX_PRELOAD_APPSADAPTER = 100;
+object GlideConstants {
+ const val MAX_PRELOAD_FILES: Int = 50
+ const val MAX_PRELOAD_APPSADAPTER: Int = 100
+ const val MAX_PRELOAD_TERMINAL_APPS: Int = 3
}
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..5c1dc0bd8b
--- /dev/null
+++ b/app/src/main/java/com/amaze/filemanager/utils/OpenTerminalUtilsExt.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
+ * Emmanuel Messulam, Raymond Lai and Contributors.
+ *
+ * This file is part of Amaze File Manager.
+ *
+ * Amaze File Manager is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.amaze.filemanager.utils
+
+import android.content.pm.PackageManager.MATCH_DEFAULT_ONLY
+import com.amaze.filemanager.ui.activities.MainActivity
+
+const val TERMONE_PLUS = "com.termoneplus"
+const val ANDROID_TERM = "jackpal.androidterm"
+const val TERMUX = "com.termux"
+
+/**
+ * Extension function to detect installed Terminal apps.
+ *
+ * Termux, Termone plus (Android terminal) and its predecessor by Jack Palovich are supported.
+ */
+fun MainActivity.detectInstalledTerminalApps(): Array {
+ val retval = ArrayList()
+ for (pkg in arrayOf(TERMONE_PLUS, ANDROID_TERM, TERMUX)) {
+ packageManager.getLaunchIntentForPackage(pkg)?.run {
+ val resolveInfos = packageManager.queryIntentActivitiesCompat(this, MATCH_DEFAULT_ONLY)
+ if (resolveInfos.isNotEmpty()
+ ) {
+ retval.add(pkg)
+ }
+ }
+ }
+ return retval.toTypedArray()
+}
diff --git a/app/src/main/java/com/amaze/filemanager/utils/PackageManagerCompatExt.kt b/app/src/main/java/com/amaze/filemanager/utils/PackageManagerCompatExt.kt
new file mode 100644
index 0000000000..e71a09c7c8
--- /dev/null
+++ b/app/src/main/java/com/amaze/filemanager/utils/PackageManagerCompatExt.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
+ * Emmanuel Messulam, Raymond Lai and Contributors.
+ *
+ * This file is part of Amaze File Manager.
+ *
+ * Amaze File Manager is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.amaze.filemanager.utils
+
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.TIRAMISU
+
+/**
+ * Wraps [PackageManager.queryIntentActivities] to SDK compatibility.
+ */
+fun PackageManager.queryIntentActivitiesCompat(
+ intent: Intent,
+ resolveInfoFlags: Int,
+): List {
+ return if (SDK_INT >= TIRAMISU) {
+ queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(resolveInfoFlags.toLong()))
+ } else {
+ queryIntentActivities(intent, resolveInfoFlags)
+ }
+}
+
+/**
+ * Wraps [PackageManager.getPackageInfo] to SDK compatibility.
+ */
+fun PackageManager.getPackageInfoCompat(
+ pkg: String,
+ packageInfoFlags: Int,
+): PackageInfo {
+ return if (SDK_INT >= TIRAMISU) {
+ getPackageInfo(pkg, PackageManager.PackageInfoFlags.of(packageInfoFlags.toLong()))
+ } else {
+ getPackageInfo(pkg, packageInfoFlags)
+ }
+}
+
+/**
+ * Wraps [PackageManager.getApplicationInfo] to SDK compatibility.
+ */
+fun PackageManager.getApplicationInfoCompat(
+ pkg: String,
+ applicationInfoFlags: Int,
+): ApplicationInfo {
+ return if (SDK_INT >= TIRAMISU) {
+ getApplicationInfo(pkg, PackageManager.ApplicationInfoFlags.of(applicationInfoFlags.toLong()))
+ } else {
+ getApplicationInfo(pkg, applicationInfoFlags)
+ }
+}
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..0cd189c52f 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
ⓘ
@@ -655,6 +656,7 @@
%d files saved.
The created file will be hidden in the file list.
App needs permission to install apps continue.
+ App needs permission to execute commands with the selected Terminal app.
Time remaining
Transfer rate
unknown
diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt
index bba0121b90..5ecb6b643e 100644
--- a/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt
+++ b/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt
@@ -73,7 +73,7 @@ abstract class AbstractMainActivityTestBase {
@NonNull
@JvmField
@RequiresApi(Build.VERSION_CODES.R)
- val allFilesPermissionRule =
+ val allFilesPermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
/**
diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractOpenFolderInTerminalTestBase.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractOpenFolderInTerminalTestBase.kt
new file mode 100644
index 0000000000..f89e7dad1a
--- /dev/null
+++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractOpenFolderInTerminalTestBase.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
+ * Emmanuel Messulam, Raymond Lai and Contributors.
+ *
+ * This file is part of Amaze File Manager.
+ *
+ * Amaze File Manager is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.amaze.filemanager.ui.dialogs
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
+import android.os.Build.VERSION_CODES.KITKAT
+import android.os.Build.VERSION_CODES.P
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import com.amaze.filemanager.shadows.ShadowMultiDex
+import com.amaze.filemanager.test.ShadowTabHandler
+import com.amaze.filemanager.ui.activities.AbstractMainActivityTestBase
+import com.amaze.filemanager.ui.activities.MainActivity
+import io.mockk.spyk
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowLooper
+import org.robolectric.shadows.ShadowPackageManager
+import org.robolectric.shadows.ShadowStorageManager
+
+@Config(
+ sdk = [KITKAT, P, Build.VERSION_CODES.R],
+ shadows = [
+ ShadowMultiDex::class,
+ ShadowTabHandler::class,
+ ShadowStorageManager::class,
+ ShadowPackageManager::class,
+ ],
+)
+abstract class AbstractOpenFolderInTerminalTestBase : AbstractMainActivityTestBase() {
+ /**
+ * Note: this method will provide a MainActivity spy for the Lambda to work with
+ */
+ protected fun doTestWithMainActivity(withMainActivity: (MainActivity) -> Unit) {
+ val scenario = ActivityScenario.launch(MainActivity::class.java)
+ ShadowLooper.idleMainLooper()
+ scenario.moveToState(Lifecycle.State.STARTED)
+ scenario.onActivity { activity ->
+ val spy = spyk(activity)
+ withMainActivity.invoke(spy)
+ scenario.moveToState(Lifecycle.State.DESTROYED)
+ scenario.close()
+ }
+ }
+
+ protected fun installApp(
+ mainActivity: MainActivity,
+ componentName: ComponentName,
+ ) {
+ shadowOf(mainActivity.packageManager).run {
+ val intentFilter: IntentFilter =
+ IntentFilter(Intent.ACTION_MAIN).also {
+ it.addCategory(Intent.CATEGORY_LAUNCHER)
+ }
+ addActivityIfNotPresent(componentName)
+ addIntentFilterForActivity(componentName, intentFilter)
+ }
+ }
+}
diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/OpenFolderInTerminalFragmentTest.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/OpenFolderInTerminalFragmentTest.kt
new file mode 100644
index 0000000000..d36c732e9d
--- /dev/null
+++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/OpenFolderInTerminalFragmentTest.kt
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
+ * Emmanuel Messulam, Raymond Lai and Contributors.
+ *
+ * This file is part of Amaze File Manager.
+ *
+ * Amaze File Manager is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.amaze.filemanager.ui.dialogs
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Build.VERSION.SDK_INT
+import android.view.View
+import androidx.test.core.app.ApplicationProvider
+import com.amaze.filemanager.adapters.holders.AppHolder
+import com.amaze.filemanager.ui.activities.MainActivity
+import com.amaze.filemanager.ui.dialogs.OpenFolderInTerminalFragment.Companion.KEY_PREFERENCES_DEFAULT
+import com.amaze.filemanager.ui.dialogs.OpenFolderInTerminalFragment.Companion.KEY_PREFERENCES_LAST
+import io.mockk.Called
+import io.mockk.CapturingSlot
+import io.mockk.every
+import io.mockk.slot
+import io.mockk.verify
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.shadows.ShadowApplication
+import org.robolectric.shadows.ShadowToast
+
+/**
+ * Tests for [OpenFolderInTerminalFragment].
+ */
+@Suppress("StringLiteralDuplication", "ComplexMethod", "LongMethod")
+class OpenFolderInTerminalFragmentTest : AbstractOpenFolderInTerminalTestBase() {
+ @Before
+ override fun setUp() {
+ super.setUp()
+ ShadowToast.reset()
+ val application = ApplicationProvider.getApplicationContext()
+ val app: ShadowApplication = shadowOf(application)
+ app.grantPermissions(
+ "com.termux.permission.RUN_COMMAND",
+ "com.termoneplus.permission.RUN_SCRIPT",
+ "jackpal.androidterm.permission.RUN_SCRIPT",
+ )
+ }
+
+ /**
+ * Test clearPreferences when no keys are found.
+ */
+ @Test
+ fun testClearPreferencesWhenNoKeyIsSet() {
+ doTestWithMainActivity { mainActivity ->
+ mainActivity.prefs.let { prefs ->
+ prefs.edit().putString("FOO", "BAR").apply()
+ assertFalse(prefs.contains(KEY_PREFERENCES_DEFAULT))
+ assertFalse(prefs.contains(KEY_PREFERENCES_LAST))
+ OpenFolderInTerminalFragment.clearPreferences(prefs)
+ assertFalse(prefs.contains(KEY_PREFERENCES_DEFAULT))
+ assertFalse(prefs.contains(KEY_PREFERENCES_LAST))
+ assertTrue(prefs.contains("FOO"))
+ }
+ }
+ }
+
+ /**
+ * Test clearPreferences when last used key is found.
+ */
+ @Test
+ fun testClearPreferencesWhenLastKeyIsSet() {
+ doTestWithMainActivity { mainActivity: MainActivity ->
+ mainActivity.prefs.let { prefs ->
+ prefs.edit()
+ .putString("FOO", "BAR")
+ .putString(KEY_PREFERENCES_LAST, "com.termoneplus")
+ .apply()
+ assertFalse(prefs.contains(KEY_PREFERENCES_DEFAULT))
+ assertTrue(prefs.contains(KEY_PREFERENCES_LAST))
+ OpenFolderInTerminalFragment.clearPreferences(prefs)
+ assertFalse(prefs.contains(KEY_PREFERENCES_DEFAULT))
+ assertFalse(prefs.contains(KEY_PREFERENCES_LAST))
+ assertTrue(prefs.contains("FOO"))
+ }
+ }
+ }
+
+ /**
+ * Test clearPreferences when default key is found.
+ */
+ @Test
+ fun testClearPreferencesWhenDefaultKeyIsSet() {
+ doTestWithMainActivity { mainActivity: MainActivity ->
+ mainActivity.prefs.let { prefs ->
+ prefs.edit()
+ .putString("FOO", "BAR")
+ .putString(KEY_PREFERENCES_DEFAULT, "com.termoneplus")
+ .apply()
+ assertTrue(prefs.contains(KEY_PREFERENCES_DEFAULT))
+ assertFalse(prefs.contains(KEY_PREFERENCES_LAST))
+ OpenFolderInTerminalFragment.clearPreferences(prefs)
+ assertFalse(prefs.contains(KEY_PREFERENCES_DEFAULT))
+ assertFalse(prefs.contains(KEY_PREFERENCES_LAST))
+ assertTrue(prefs.contains("FOO"))
+ }
+ }
+ }
+
+ /**
+ * Test clearPreferences when both keys are found.
+ */
+ @Test
+ fun testClearPreferencesWhenBothKeysAreSet() {
+ doTestWithMainActivity { mainActivity: MainActivity ->
+ mainActivity.prefs.let { prefs ->
+ prefs.edit()
+ .putString("FOO", "BAR")
+ .putString(KEY_PREFERENCES_DEFAULT, "com.termoneplus")
+ .putString(KEY_PREFERENCES_LAST, "com.termux")
+ .apply()
+ assertTrue(prefs.contains(KEY_PREFERENCES_DEFAULT))
+ assertTrue(prefs.contains(KEY_PREFERENCES_LAST))
+ OpenFolderInTerminalFragment.clearPreferences(prefs)
+ assertFalse(prefs.contains(KEY_PREFERENCES_DEFAULT))
+ assertFalse(prefs.contains(KEY_PREFERENCES_LAST))
+ assertTrue(prefs.contains("FOO"))
+ }
+ }
+ }
+
+ /**
+ * Test when no terminal app is installed.
+ */
+ @Test
+ fun testOpenOrShowWhenNoTerminalInstalled() {
+ doTestWithMainActivity { mainActivity: MainActivity ->
+ OpenFolderInTerminalFragment.openTerminalOrShow("/sdcard/tmp", mainActivity)
+ assertTrue(ShadowToast.shownToastCount() == 1)
+ assertEquals("No Terminal App installed", ShadowToast.getTextOfLatestToast())
+ }
+ }
+
+ private fun `After install specified app`(
+ componentName: ComponentName,
+ beforeOpen: ((MainActivity, CapturingSlot) -> Unit)? = null,
+ nextStep: (MainActivity, CapturingSlot) -> Unit,
+ ) {
+ doTestWithMainActivity { mainActivity: MainActivity ->
+ installApp(mainActivity, componentName)
+ val capturedIntent = slot()
+ val capturedCallback = slot<() -> Unit>()
+ every {
+ mainActivity.requestTerminalPermission(any(), capture(capturedCallback))
+ } answers {
+ capturedCallback.captured.invoke()
+ }
+ every {
+ mainActivity.startActivity(capture(capturedIntent), any())
+ } answers {
+ callOriginal()
+ }
+ beforeOpen?.invoke(mainActivity, capturedIntent)
+ OpenFolderInTerminalFragment.openTerminalOrShow("/sdcard/tmp", mainActivity)
+ nextStep.invoke(mainActivity, capturedIntent)
+ }
+ }
+
+ /**
+ * Test when only Termone Plus is installed.
+ */
+ @Test
+ fun testOpenTerminalWhenOnlyTermonePlusIsInstalled() {
+ `After install specified app`(
+ ComponentName("com.termoneplus", "com.termoneplus.Activity"),
+ ) { mainActivity, capturedIntent ->
+ verify {
+ mainActivity.startActivity(capturedIntent.captured, null)
+ }
+
+ capturedIntent.captured.let { intent ->
+ assertEquals("com.termoneplus.RUN_SCRIPT", intent.action)
+ assertEquals("com.termoneplus", intent.component?.packageName)
+ assertEquals("jackpal.androidterm.RunScript", intent.component?.className)
+ assertEquals(
+ "cd \"/sdcard/tmp\"",
+ intent.getStringExtra("com.termoneplus.Command"),
+ )
+ }
+ }
+ }
+
+ /**
+ * Test when only Termux is installed.
+ */
+ @Test
+ fun testOpenTerminalWhenOnlyTermuxIsInstalled() {
+ `After install specified app`(
+ componentName = ComponentName("com.termux", "com.termux.Activity"),
+ beforeOpen = { mainActivity, capturedIntent ->
+ if (SDK_INT >= 26) {
+ every {
+ mainActivity.startForegroundService(capture(capturedIntent))
+ } answers { callOriginal() }
+ } else {
+ every {
+ mainActivity.startService(capture(capturedIntent))
+ } answers {
+ callOriginal()
+ }
+ }
+ },
+ ) { mainActivity, capturedIntent ->
+ verify {
+ if (SDK_INT >= 26) {
+ mainActivity.startForegroundService(capturedIntent.captured)
+ mainActivity.startService(capturedIntent.captured)?.wasNot(Called)
+ } else {
+ mainActivity.startService(capturedIntent.captured)
+ }
+ }
+
+ capturedIntent.captured.let { intent ->
+ assertEquals("com.termux.RUN_COMMAND", intent.action)
+ assertEquals("com.termux", intent.component?.packageName)
+ assertEquals("com.termux.app.RunCommandService", intent.component?.className)
+ }
+ }
+ }
+
+ private fun `After setup case of both Termux and Termone plus installed`(
+ beforeOpen: ((MainActivity) -> Unit)? = null,
+ nextStep: (MainActivity, CapturingSlot) -> Unit,
+ ) {
+ doTestWithMainActivity { mainActivity ->
+ installApp(mainActivity, ComponentName("com.termoneplus", "com.termoneplus.Activity"))
+ installApp(mainActivity, ComponentName("com.termux", "com.termux.Activity"))
+ val capturedIntent = slot()
+ val capturedCallback = slot<() -> Unit>()
+ every {
+ mainActivity.startActivity(capture(capturedIntent), any())
+ } answers {
+ callOriginal()
+ }
+ every {
+ mainActivity.requestTerminalPermission(any(), capture(capturedCallback))
+ } answers {
+ capturedCallback.captured.invoke()
+ }
+ beforeOpen?.invoke(mainActivity)
+ OpenFolderInTerminalFragment.openTerminalOrShow("/sdcard/tmp", mainActivity)
+ nextStep.invoke(mainActivity, capturedIntent)
+ }
+ }
+
+ /**
+ * When Termux and Termone plus are installed, but default is set to Termone Plus.
+ */
+ @Test
+ fun `When Termux and Termone plus are installed, but default is set to Termone Plus`() {
+ `After setup case of both Termux and Termone plus installed`(
+ beforeOpen = { mainActivity: MainActivity ->
+ mainActivity.prefs.edit().putString("terminal._DEFAULT", "com.termoneplus").apply()
+ },
+ nextStep = { mainActivity, capturedIntent ->
+ verify {
+ mainActivity.startActivity(capturedIntent.captured, null)
+ }
+ capturedIntent.captured.let { intent ->
+ assertEquals("com.termoneplus.RUN_SCRIPT", intent.action)
+ assertEquals("com.termoneplus", intent.component?.packageName)
+ assertEquals("jackpal.androidterm.RunScript", intent.component?.className)
+ assertEquals(
+ "cd \"/sdcard/tmp\"",
+ intent.getStringExtra("com.termoneplus.Command"),
+ )
+ }
+ },
+ )
+ }
+
+ /**
+ * Test Dialog fragment instance.
+ */
+ @Test
+ fun `Display dialog fragment, choosing always use Termone plus`() {
+ `After setup case of both Termux and Termone plus installed` { mainActivity, capturedIntent ->
+ assertTrue(mainActivity.supportFragmentManager.executePendingTransactions())
+ mainActivity.supportFragmentManager.fragments.last().run {
+ assertTrue(this is OpenFolderInTerminalFragment)
+ (this as OpenFolderInTerminalFragment).let { fragment ->
+ // Because one item had been removed to the last app
+ assertEquals(1, viewBinding.appsRecyclerView.adapter?.itemCount)
+ assertEquals("com.termoneplus", viewBinding.lastAppTitle.text)
+ fragment.viewBinding.alwaysButton.performClick()
+ }
+ }
+ assertEquals(
+ "com.termoneplus",
+ mainActivity.prefs.getString(KEY_PREFERENCES_DEFAULT, null),
+ )
+
+ OpenFolderInTerminalFragment.openTerminalOrShow("/sdcard/tmp", mainActivity)
+
+ verify {
+ mainActivity.startActivity(capturedIntent.captured, null)
+ }
+
+ capturedIntent.captured.let { intent ->
+ assertEquals("com.termoneplus.RUN_SCRIPT", intent.action)
+ assertEquals("com.termoneplus", intent.component?.packageName)
+ assertEquals("jackpal.androidterm.RunScript", intent.component?.className)
+ assertEquals(
+ "cd \"/sdcard/tmp\"",
+ intent.getStringExtra("com.termoneplus.Command"),
+ )
+ }
+ }
+ }
+
+ /**
+ * Test Dialog fragment instance.
+ */
+ @Test
+ fun `Display dialog fragment, choosing use Termone plus once`() {
+ `After setup case of both Termux and Termone plus installed` { mainActivity, capturedIntent ->
+ assertTrue(mainActivity.supportFragmentManager.executePendingTransactions())
+ mainActivity.supportFragmentManager.fragments.last().run {
+ assertTrue(this is OpenFolderInTerminalFragment)
+ (this as OpenFolderInTerminalFragment).let { fragment ->
+ // Because one item had been removed to the last app
+ assertEquals(1, viewBinding.appsRecyclerView.adapter?.itemCount)
+ assertEquals("com.termoneplus", viewBinding.lastAppTitle.text)
+ fragment.viewBinding.justOnceButton.performClick()
+ }
+ }
+ assertEquals(
+ "com.termoneplus",
+ mainActivity.prefs.getString(KEY_PREFERENCES_LAST, null),
+ )
+ }
+ }
+
+ /**
+ * Test when both Termone plus and Termux are found, but user choose to use Termux once only.
+ */
+ @Test
+ fun `With both Termux and Termone plus, choose Termux but once only`() {
+ `After setup case of both Termux and Termone plus installed` { mainActivity, capturedIntent ->
+ assertTrue(mainActivity.supportFragmentManager.executePendingTransactions())
+ mainActivity.supportFragmentManager.fragments.last().run {
+ assertTrue(this is OpenFolderInTerminalFragment)
+ (this as OpenFolderInTerminalFragment).let { fragment ->
+ // Because one item had been removed to the last app
+ assertEquals(1, viewBinding.appsRecyclerView.adapter?.itemCount)
+ assertEquals("com.termoneplus", viewBinding.lastAppTitle.text)
+ assertEquals(1, fragment.viewBinding.appsRecyclerView.adapter?.itemCount)
+ fragment.viewBinding.appsRecyclerView.run {
+ measure(
+ View.MeasureSpec.UNSPECIFIED,
+ View.MeasureSpec.UNSPECIFIED,
+ )
+ layout(0, 0, 1000, 1000)
+ val viewHolder = findViewHolderForAdapterPosition(0)
+ assertNotNull(viewHolder)
+ (viewHolder as AppHolder).run {
+ assertEquals("com.termux", txtTitle.text)
+ rl.performClick()
+ }
+ }
+ }
+ }
+ assertEquals(
+ "com.termux",
+ mainActivity.prefs.getString(KEY_PREFERENCES_LAST, null),
+ )
+ OpenFolderInTerminalFragment.openTerminalOrShow("/sdcard/tmp", mainActivity)
+ assertTrue(mainActivity.supportFragmentManager.executePendingTransactions())
+ mainActivity.supportFragmentManager.fragments.last().run {
+ assertTrue(this is OpenFolderInTerminalFragment)
+ (this as OpenFolderInTerminalFragment).let { fragment ->
+ // Because one item had been removed to the last app
+ assertEquals(1, viewBinding.appsRecyclerView.adapter?.itemCount)
+ assertEquals("com.termux", viewBinding.lastAppTitle.text)
+
+ assertEquals(1, fragment.viewBinding.appsRecyclerView.adapter?.itemCount)
+ fragment.viewBinding.appsRecyclerView.run {
+ measure(
+ View.MeasureSpec.UNSPECIFIED,
+ View.MeasureSpec.UNSPECIFIED,
+ )
+ layout(0, 0, 1000, 1000)
+ val viewHolder = findViewHolderForAdapterPosition(0)
+ assertNotNull(viewHolder)
+ (viewHolder as AppHolder).run {
+ assertEquals("com.termoneplus", txtTitle.text)
+ rl.performClick()
+ }
+ }
+ }
+ }
+ assertEquals(
+ "com.termoneplus",
+ mainActivity.prefs.getString(KEY_PREFERENCES_LAST, null),
+ )
+ OpenFolderInTerminalFragment.openTerminalOrShow("/sdcard/tmp", mainActivity)
+ assertTrue(mainActivity.supportFragmentManager.executePendingTransactions())
+ mainActivity.supportFragmentManager.fragments.last().run {
+ assertTrue(this is OpenFolderInTerminalFragment)
+ (this as OpenFolderInTerminalFragment).let { fragment ->
+ // Because one item had been removed to the last app
+ assertEquals(1, viewBinding.appsRecyclerView.adapter?.itemCount)
+ assertEquals("com.termoneplus", viewBinding.lastAppTitle.text)
+ assertEquals(1, fragment.viewBinding.appsRecyclerView.adapter?.itemCount)
+ fragment.viewBinding.appsRecyclerView.run {
+ measure(
+ View.MeasureSpec.UNSPECIFIED,
+ View.MeasureSpec.UNSPECIFIED,
+ )
+ layout(0, 0, 1000, 1000)
+ val viewHolder = findViewHolderForAdapterPosition(0)
+ assertNotNull(viewHolder)
+ (viewHolder as AppHolder).run {
+ assertEquals("com.termux", txtTitle.text)
+ }
+ }
+ fragment.viewBinding.alwaysButton.performClick()
+ }
+ }
+ assertEquals(
+ "com.termoneplus",
+ mainActivity.prefs.getString(KEY_PREFERENCES_DEFAULT, null),
+ )
+ OpenFolderInTerminalFragment.openTerminalOrShow("/sdcard/tmp", mainActivity)
+
+ verify {
+ mainActivity.startActivity(capturedIntent.captured, null)
+ }
+
+ capturedIntent.captured.let { intent ->
+ assertEquals("com.termoneplus.RUN_SCRIPT", intent.action)
+ assertEquals("com.termoneplus", intent.component?.packageName)
+ assertEquals("jackpal.androidterm.RunScript", intent.component?.className)
+ assertEquals(
+ "cd \"/sdcard/tmp\"",
+ intent.getStringExtra("com.termoneplus.Command"),
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/test/java/com/amaze/filemanager/utils/OpenTerminalUtilsExtTest.kt b/app/src/test/java/com/amaze/filemanager/utils/OpenTerminalUtilsExtTest.kt
new file mode 100644
index 0000000000..f1829d6f7f
--- /dev/null
+++ b/app/src/test/java/com/amaze/filemanager/utils/OpenTerminalUtilsExtTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra ,
+ * Emmanuel Messulam, Raymond Lai and Contributors.
+ *
+ * This file is part of Amaze File Manager.
+ *
+ * Amaze File Manager is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.amaze.filemanager.utils
+
+import android.content.ComponentName
+import com.amaze.filemanager.ui.activities.MainActivity
+import com.amaze.filemanager.ui.dialogs.AbstractOpenFolderInTerminalTestBase
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+/**
+ * Test [MainActivity.detectInstalledTerminalApps] in extension functions.
+ */
+@Suppress("StringLiteralDuplication")
+class OpenTerminalUtilsExtTest : AbstractOpenFolderInTerminalTestBase() {
+ /**
+ * Case when no supported Terminal is installed.
+ */
+ @Test
+ fun `Test when there is no terminal app installed`() {
+ doTestWithMainActivity { mainActivity ->
+ val result = mainActivity.detectInstalledTerminalApps()
+ assertNotNull(result)
+ assertEquals(0, result.size)
+ }
+ }
+
+ /**
+ * Case when Termux is installed.
+ */
+ @Test
+ fun `Test when there is only Termux installed`() {
+ doTestWithMainActivity { mainActivity ->
+ // Package name is important. Class name is not... no need to 100% match
+ installApp(mainActivity, ComponentName("com.termux", "com.termux.Activity"))
+
+ val result = mainActivity.detectInstalledTerminalApps()
+ assertNotNull(result)
+ assertEquals(1, result.size)
+ assertEquals("com.termux", result.first())
+ }
+ }
+
+ /**
+ * Case when both Termux and Termone plus are installed.
+ */
+ @Test
+ fun `Test when there are both Termux and Termone plus installed`() {
+ doTestWithMainActivity { mainActivity ->
+ // Package name is important. Class name is not... no need to 100% match
+ installApp(mainActivity, ComponentName("com.termux", "com.termux.Activity"))
+ installApp(mainActivity, ComponentName("com.termoneplus", "com.termoneplus.Activity"))
+
+ val result = mainActivity.detectInstalledTerminalApps()
+ assertNotNull(result)
+ assertEquals(2, result.size)
+ assertTrue(result.contains("com.termux"))
+ assertTrue(result.contains("com.termoneplus"))
+ }
+ }
+
+ /**
+ * Real life situation, when Termux and Termone plus are installed among others.
+ */
+ @Test
+ fun `Test when there are other apps installed, method should filter them out`() {
+ doTestWithMainActivity { mainActivity ->
+ // Package name is important. Class name is not... no need to 100% match
+ installApp(mainActivity, ComponentName("com.termux", "com.termux.Activity"))
+ installApp(mainActivity, ComponentName("com.termoneplus", "com.termoneplus.Activity"))
+ installApp(mainActivity, ComponentName("com.amaze.filemanager", "com.amaze.filemanager.Activity"))
+
+ val result = mainActivity.detectInstalledTerminalApps()
+ assertNotNull(result)
+ assertEquals(2, result.size)
+ assertTrue(result.contains("com.termux"))
+ assertTrue(result.contains("com.termoneplus"))
+ }
+ }
+}