From d4ed8568cd3c6a3fbbd541b8decc7fd4a0482c57 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Tue, 25 Apr 2023 22:56:17 +0800 Subject: [PATCH] Add context menu "Open in Terminal" option Fixes #2666 --- app/src/main/AndroidManifest.xml | 3 + .../adapters/AppsRecyclerAdapter.kt | 13 +- .../filemanager/adapters/RecyclerAdapter.java | 2 + .../filemanager/filesystem/HybridFile.java | 11 + .../filesystem/cloud/CloudUtil.java | 4 + .../filesystem/files/FileUtils.java | 6 +- .../amaze/filemanager/ui/ItemPopupMenu.java | 4 + .../ui/activities/MainActivity.java | 15 +- .../{BasicActivity.java => BasicActivity.kt} | 39 +- .../superclasses/PermissionsActivity.java | 297 ----------- .../superclasses/PermissionsActivity.kt | 385 +++++++++++++++ .../superclasses/PreferenceActivity.java | 121 ----- .../superclasses/PreferenceActivity.kt | 111 +++++ .../ui/dialogs/OpenFileDialogFragment.kt | 7 +- .../dialogs/OpenFolderInTerminalFragment.kt | 387 +++++++++++++++ .../ui/fragments/AppsListFragment.java | 2 +- .../BehaviorPrefsFragment.kt | 6 +- ...{GlideConstants.java => GlideConstants.kt} | 11 +- .../utils/MainActivityActionMode.kt | 5 + .../filemanager/utils/OpenTerminalUtilsExt.kt | 46 ++ .../utils/PackageManagerCompatExt.kt | 70 +++ app/src/main/res/menu/activity_extra.xml | 3 + app/src/main/res/menu/item_extras.xml | 3 + app/src/main/res/values/strings.xml | 2 + .../AbstractMainActivityTestBase.kt | 2 +- .../AbstractOpenFolderInTerminalTestBase.kt | 79 +++ .../OpenFolderInTerminalFragmentTest.kt | 466 ++++++++++++++++++ .../utils/OpenTerminalUtilsExtTest.kt | 99 ++++ 28 files changed, 1739 insertions(+), 460 deletions(-) rename app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/{BasicActivity.java => BasicActivity.kt} (52%) delete mode 100644 app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java create mode 100644 app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.kt delete mode 100644 app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PreferenceActivity.java create mode 100644 app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PreferenceActivity.kt create mode 100644 app/src/main/java/com/amaze/filemanager/ui/dialogs/OpenFolderInTerminalFragment.kt rename app/src/main/java/com/amaze/filemanager/utils/{GlideConstants.java => GlideConstants.kt} (83%) create mode 100644 app/src/main/java/com/amaze/filemanager/utils/OpenTerminalUtilsExt.kt create mode 100644 app/src/main/java/com/amaze/filemanager/utils/PackageManagerCompatExt.kt create mode 100644 app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractOpenFolderInTerminalTestBase.kt create mode 100644 app/src/test/java/com/amaze/filemanager/ui/dialogs/OpenFolderInTerminalFragmentTest.kt create mode 100644 app/src/test/java/com/amaze/filemanager/utils/OpenTerminalUtilsExtTest.kt 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..d89868a232 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.kt @@ -0,0 +1,385 @@ +/* + * 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) + @Suppress("Detekt.TooGenericExceptionCaught") + 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")) + } + } +}