From 89fb389723aa54184b0873a7aefa01e2137919c3 Mon Sep 17 00:00:00 2001 From: arthur-star <53738161+arthur-star@users.noreply.github.com> Date: Thu, 14 Oct 2021 18:08:46 +0800 Subject: [PATCH 01/26] fix string casting --- .../com/amaze/filemanager/asynchronous/handlers/FileHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt index 853cab53e0..09f78bb9f4 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt @@ -50,7 +50,7 @@ class FileHandler( return } - val path = msg.obj as String + val path = (msg.obj as? String) ?: "" when (msg.what) { CustomFileObserver.GOBACK -> { main.goBack() From 671edfae7167d32b16378274e3b684364876fda2 Mon Sep 17 00:00:00 2001 From: arthur-star <53738161+arthur-star@users.noreply.github.com> Date: Fri, 15 Oct 2021 10:58:41 +0800 Subject: [PATCH 02/26] path change to nullable --- .../filemanager/asynchronous/handlers/FileHandler.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt index 09f78bb9f4..e8d568d570 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt @@ -24,6 +24,7 @@ import android.os.Handler import android.os.Looper import android.os.Message import android.view.View +import android.util.Log import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.adapters.RecyclerAdapter import com.amaze.filemanager.filesystem.CustomFileObserver @@ -41,6 +42,10 @@ class FileHandler( ) { private val mainFragment: WeakReference = WeakReference(mainFragment) + companion object { + private val TAG = FileHandler::class.java.simpleName + } + override fun handleMessage(msg: Message) { super.handleMessage(msg) val main = mainFragment.get() ?: return @@ -50,12 +55,16 @@ class FileHandler( return } - val path = (msg.obj as? String) ?: "" + val path = msg.obj as? String when (msg.what) { CustomFileObserver.GOBACK -> { main.goBack() } CustomFileObserver.NEW_ITEM -> { + if (path == null){ + Log.e(TAG, "Path is empty for file") + return + } val fileCreated = HybridFile( mainFragmentViewModel.openMode, "${main.currentPath}/$path" ) From 4f02f3153911cf6757be7edf44d6874186927eea Mon Sep 17 00:00:00 2001 From: arthur-star <53738161+arthur-star@users.noreply.github.com> Date: Sat, 16 Oct 2021 21:39:15 +0800 Subject: [PATCH 03/26] update by spotlessApply --- .../amaze/filemanager/asynchronous/handlers/FileHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt index e8d568d570..a8f85ae0c2 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt @@ -23,8 +23,8 @@ package com.amaze.filemanager.asynchronous.handlers import android.os.Handler import android.os.Looper import android.os.Message -import android.view.View import android.util.Log +import android.view.View import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.adapters.RecyclerAdapter import com.amaze.filemanager.filesystem.CustomFileObserver @@ -61,7 +61,7 @@ class FileHandler( main.goBack() } CustomFileObserver.NEW_ITEM -> { - if (path == null){ + if (path == null) { Log.e(TAG, "Path is empty for file") return } From 256b16b269afdb37a0c465e66f5425380db6de4e Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sun, 3 Oct 2021 00:31:37 +0800 Subject: [PATCH 04/26] Fix SSH with password authentication Fixes #2843. - Fix SftpEntryDao SQL declarations. sqlite is case insensitive, but just to be sure - Use Kotlin's null/empty check in SshAuthenticationTask and SshConnectionPool - Trim trailing \n when encrypting SSH passwords - returns SSH path with encrypted password out to Drawer when calling UtilsHandler.getSftpList() - Fix handling of encoded passwords at SshClientUtils.extractBaseUriFrom(), use our own parsing routine instead of Uri.parse which will break when encrypted password contains slash characters --- .../asynctasks/LoadFilesListTask.java | 2 +- .../asynctasks/ssh/SshAuthenticationTask.kt | 2 +- .../filemanager/database/UtilsHandler.java | 28 +++--- .../database/daos/SftpEntryDao.java | 15 ++- .../filemanager/filesystem/HybridFile.java | 15 ++- .../filesystem/ssh/SshClientUtils.java | 23 +++-- .../filesystem/ssh/SshConnectionPool.kt | 26 ++++- .../ui/activities/MainActivity.java | 20 ++-- .../ui/dialogs/SftpConnectDialog.kt | 41 ++++---- .../asynctasks/ssh/PemToKeyPairTaskTest.java | 0 .../asynctasks/ssh/PemToKeyPairTaskTest2.java | 0 .../filesystem/ssh/SshConnectionPoolTest.java | 71 +++++++++----- .../filemanager/ssh/SshClientUtilTest.java | 38 -------- .../filemanager/ssh/SshClientUtilTest.kt | 94 +++++++++++++++++++ .../filemanager/test/ShadowCryptUtilTest.java | 4 +- .../ui/activities/MainActivityTest.java | 78 +++++++++++++++ build.gradle | 2 +- 17 files changed, 331 insertions(+), 128 deletions(-) rename app/src/{androidTest => test}/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest.java (100%) rename app/src/{androidTest => test}/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest2.java (100%) delete mode 100644 app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.java create mode 100644 app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.kt diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java index 71d22aea9f..536555da1f 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java @@ -151,7 +151,7 @@ public LoadFilesListTask( case SFTP: HybridFile sftpHFile = new HybridFile(OpenMode.SFTP, path); - list = new ArrayList(); + list = new ArrayList(); sftpHFile.forEachChildrenFile( context, diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTask.kt index 16f5bbde9c..4eeed4af7b 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTask.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTask.kt @@ -79,7 +79,7 @@ class SshAuthenticationTask( } return runCatching { sshClient.connect(hostname, port) - if (password != null && "" != password) { + if (true == password?.isNotEmpty()) { sshClient.authPassword(username, password) AsyncTaskResult(sshClient) } else { diff --git a/app/src/main/java/com/amaze/filemanager/database/UtilsHandler.java b/app/src/main/java/com/amaze/filemanager/database/UtilsHandler.java index 8ab31e840c..45ea1ece80 100644 --- a/app/src/main/java/com/amaze/filemanager/database/UtilsHandler.java +++ b/app/src/main/java/com/amaze/filemanager/database/UtilsHandler.java @@ -38,7 +38,6 @@ import com.amaze.filemanager.database.models.utilities.History; import com.amaze.filemanager.database.models.utilities.SftpEntry; import com.amaze.filemanager.database.models.utilities.SmbEntry; -import com.amaze.filemanager.filesystem.ssh.SshClientUtils; import com.amaze.filemanager.utils.SmbUtil; import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree; import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFactory; @@ -302,7 +301,7 @@ public List getSftpList() { ArrayList retval = new ArrayList(); for (SftpEntry entry : utilitiesDatabase.sftpEntryDao().list().subscribeOn(Schedulers.io()).blockingGet()) { - String path = SshClientUtils.decryptSshPathAsNecessary(entry.path); + String path = entry.path; if (path == null) { Log.e("ERROR", "Error decrypting path: " + entry.path); @@ -318,22 +317,17 @@ public List getSftpList() { } public String getSshHostKey(String uri) { - uri = SshClientUtils.encryptSshPathAsNecessary(uri); - if (uri != null) { - try { - return utilitiesDatabase - .sftpEntryDao() - .getSshHostKey(uri) - .subscribeOn(Schedulers.io()) - .blockingGet(); - } catch (Exception e) { - // catch error to handle Single#onError for blockingGet - if (DEBUG) { - Log.e(getClass().getSimpleName(), "Error getting public key for URI [" + uri + "]", e); - } - return null; + try { + return utilitiesDatabase + .sftpEntryDao() + .getSshHostKey(uri) + .subscribeOn(Schedulers.io()) + .blockingGet(); + } catch (Exception e) { + // catch error to handle Single#onError for blockingGet + if (DEBUG) { + Log.e(getClass().getSimpleName(), "Error getting public key for URI [" + uri + "]", e); } - } else { return null; } } diff --git a/app/src/main/java/com/amaze/filemanager/database/daos/SftpEntryDao.java b/app/src/main/java/com/amaze/filemanager/database/daos/SftpEntryDao.java index eec4d68071..474e70d47c 100644 --- a/app/src/main/java/com/amaze/filemanager/database/daos/SftpEntryDao.java +++ b/app/src/main/java/com/amaze/filemanager/database/daos/SftpEntryDao.java @@ -72,13 +72,22 @@ public interface SftpEntryDao { @Query("SELECT * FROM " + TABLE_SFTP + " WHERE " + COLUMN_NAME + " = :name") Single findByName(String name); - @Query("SELECT " + COLUMN_HOST_PUBKEY + " FROM " + TABLE_SFTP + " WHERE PATH = :uri") + @Query( + "SELECT " + COLUMN_HOST_PUBKEY + " FROM " + TABLE_SFTP + " WHERE " + COLUMN_PATH + " = :uri") Single getSshHostKey(String uri); - @Query("SELECT " + COLUMN_PRIVATE_KEY_NAME + " FROM " + TABLE_SFTP + " WHERE PATH = :uri") + @Query( + "SELECT " + + COLUMN_PRIVATE_KEY_NAME + + " FROM " + + TABLE_SFTP + + " WHERE " + + COLUMN_PATH + + " = :uri") Single getSshAuthPrivateKeyName(String uri); - @Query("SELECT " + COLUMN_PRIVATE_KEY + " FROM " + TABLE_SFTP + " WHERE PATH = :uri") + @Query( + "SELECT " + COLUMN_PRIVATE_KEY + " FROM " + TABLE_SFTP + " WHERE " + COLUMN_PATH + " = :uri") Single getSshAuthPrivateKey(String uri); @Query("DELETE FROM " + TABLE_SFTP + " WHERE " + COLUMN_NAME + " = :name") 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 1c7d9aedeb..7274f71778 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -48,6 +48,7 @@ import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate; import com.amaze.filemanager.filesystem.ssh.SshClientTemplate; import com.amaze.filemanager.filesystem.ssh.SshClientUtils; +import com.amaze.filemanager.filesystem.ssh.SshConnectionPool; import com.amaze.filemanager.filesystem.ssh.Statvfs; import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants; import com.amaze.filemanager.utils.DataUtils; @@ -987,8 +988,18 @@ public String getReadablePath(String path) { } public static String parseAndFormatUriForDisplay(@NonNull String uriString) { - Uri uri = Uri.parse(uriString); - return String.format("%s://%s%s", uri.getScheme(), uri.getHost(), uri.getPath()); + if (uriString.startsWith(SSH_URI_PREFIX)) { + SshConnectionPool.ConnectionInfo connInfo = new SshConnectionPool.ConnectionInfo(uriString); + return connInfo.toString(); + } else { + Uri uri = Uri.parse(uriString); + return formatUriForDisplayInternal(uri.getScheme(), uri.getHost(), uri.getPath()); + } + } + + private static String formatUriForDisplayInternal( + @NonNull String scheme, @NonNull String host, @NonNull String path) { + return String.format("%s://%s%s", scheme, host, path); } /** diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.java index c9cde4a5f3..e6098e9a3a 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.java @@ -29,7 +29,6 @@ import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.List; -import java.util.concurrent.Callable; import com.amaze.filemanager.R; import com.amaze.filemanager.application.AppConfig; @@ -85,7 +84,7 @@ public static T execute(@NonNull SshClientTemplate template) { final SSHClient _client = client; try { retval = - Single.fromCallable((Callable) () -> template.execute(_client)) + Single.fromCallable(() -> template.execute(_client)) .subscribeOn(Schedulers.io()) .blockingGet(); } catch (Exception e) { @@ -181,7 +180,7 @@ public static String encryptSshPathAsNecessary(@NonNull String fullUri) { fullUri.substring(SSH_URI_PREFIX.length(), fullUri.lastIndexOf('@')); try { return (uriWithoutProtocol.lastIndexOf(':') > 0) - ? SmbUtil.getSmbEncryptedPath(AppConfig.getInstance(), fullUri) + ? SmbUtil.getSmbEncryptedPath(AppConfig.getInstance(), fullUri).replace("\n", "") : fullUri; } catch (IOException | GeneralSecurityException e) { Log.e(TAG, "Error encrypting path", e); @@ -220,9 +219,15 @@ public static String decryptSshPathAsNecessary(@NonNull String fullUri) { */ public static String extractBaseUriFrom(@NonNull String fullUri) { String uriWithoutProtocol = fullUri.substring(SSH_URI_PREFIX.length()); - return uriWithoutProtocol.indexOf('/') == -1 - ? fullUri - : fullUri.substring(0, uriWithoutProtocol.indexOf('/') + SSH_URI_PREFIX.length()); + String credentials = uriWithoutProtocol.substring(0, uriWithoutProtocol.lastIndexOf('@')); + String hostAndPath = uriWithoutProtocol.substring(uriWithoutProtocol.lastIndexOf('@') + 1); + if (hostAndPath.indexOf('/') == -1) { + return fullUri; + } else { + String host = hostAndPath.substring(0, hostAndPath.indexOf('/')); + return fullUri.substring( + 0, SSH_URI_PREFIX.length() + credentials.length() + 1 + host.length()); + } } /** @@ -235,10 +240,8 @@ public static String extractBaseUriFrom(@NonNull String fullUri) { * @return The remote path part of the full SSH URL */ public static String extractRemotePathFrom(@NonNull String fullUri) { - String uriWithoutProtocol = fullUri.substring(SSH_URI_PREFIX.length()); - return uriWithoutProtocol.indexOf('/') == -1 - ? "/" - : uriWithoutProtocol.substring(uriWithoutProtocol.indexOf('/')); + String hostPath = fullUri.substring(fullUri.lastIndexOf('@')); + return hostPath.indexOf('/') == -1 ? "/" : hostPath.substring(hostPath.indexOf('/')); } /** diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshConnectionPool.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshConnectionPool.kt index 54985d0220..124035e892 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshConnectionPool.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshConnectionPool.kt @@ -25,6 +25,7 @@ import android.util.Log import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.asynchronous.asynctasks.ssh.PemToKeyPairTask import com.amaze.filemanager.asynchronous.asynctasks.ssh.SshAuthenticationTask +import com.amaze.filemanager.filesystem.files.CryptUtil import net.schmizz.sshj.Config import net.schmizz.sshj.SSHClient import java.security.KeyPair @@ -174,7 +175,7 @@ object SshConnectionPool { val utilsHandler = AppConfig.getInstance().utilsHandler val pem = utilsHandler.getSshAuthPrivateKey(url) val keyPair = AtomicReference(null) - if (pem != null && !pem.isEmpty()) { + if (true == pem?.isNotEmpty()) { try { val latch = CountDownLatch(1) PemToKeyPairTask( @@ -245,12 +246,12 @@ object SshConnectionPool { * * A design decision to keep database schema slim, by the way... -TranceLove */ - internal class ConnectionInfo(url: String) { + class ConnectionInfo(url: String) { val host: String val port: Int val username: String val password: String? - protected var defaultPath: String? = null + var defaultPath: String? = null // FIXME: Crude assumption init { @@ -269,10 +270,27 @@ object SshConnectionPool { val authString = url.substring(SSH_URI_PREFIX.length, url.lastIndexOf('@')) val userInfo = authString.split(":").toTypedArray() username = userInfo[0] - password = if (userInfo.size > 1) userInfo[1] else null + password = if (userInfo.size > 1) { + runCatching { + CryptUtil.decryptPassword(AppConfig.getInstance(), userInfo[1]) + }.getOrElse { + /* Hack. It should only happen after creating new SSH connection settings + * and plain text password is sent in. + * + * Possible to encrypt password there as alternate solution. + */ + userInfo[1] + } + } else { + null + } if (port < 0) port = SSH_DEFAULT_PORT this.port = port } + + override fun toString(): String { + return "${SSH_URI_PREFIX}$username@$host:$port${defaultPath ?: ""}" + } } class AsyncRemoveConnection internal constructor( 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 71a25f067e..83fddcbbf7 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 @@ -1878,25 +1878,23 @@ public void showSftpDialog(String name, String path, boolean edit) { if (i != -1) name = dataUtils.getServers().get(i)[0]; } SftpConnectDialog sftpConnectDialog = new SftpConnectDialog(); - Uri uri = Uri.parse(path); - String userinfo = uri.getUserInfo(); + SshConnectionPool.ConnectionInfo connInfo = new SshConnectionPool.ConnectionInfo(path); + Bundle bundle = new Bundle(); bundle.putString(ARG_NAME, name); - bundle.putString(ARG_ADDRESS, uri.getHost()); - bundle.putInt(ARG_PORT, uri.getPort()); - if (!TextUtils.isEmpty(uri.getPath())) { - bundle.putString(ARG_DEFAULT_PATH, uri.getPath()); + bundle.putString(ARG_ADDRESS, connInfo.getHost()); + bundle.putInt(ARG_PORT, connInfo.getPort()); + if (!TextUtils.isEmpty(connInfo.getDefaultPath())) { + bundle.putString(ARG_DEFAULT_PATH, connInfo.getDefaultPath()); } - bundle.putString( - ARG_USERNAME, - userinfo.indexOf(':') > 0 ? userinfo.substring(0, userinfo.indexOf(':')) : userinfo); + bundle.putString(ARG_USERNAME, connInfo.getUsername()); - if (userinfo.indexOf(':') < 0) { + if (connInfo.getPassword() == null) { bundle.putBoolean(ARG_HAS_PASSWORD, false); bundle.putString(ARG_KEYPAIR_NAME, utilsHandler.getSshAuthPrivateKeyName(path)); } else { bundle.putBoolean(ARG_HAS_PASSWORD, true); - bundle.putString(ARG_PASSWORD, userinfo.substring(userinfo.indexOf(':') + 1)); + bundle.putString(ARG_PASSWORD, connInfo.getPassword()); } bundle.putBoolean(ARG_EDIT, edit); sftpConnectDialog.setArguments(bundle); diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt index 04ad5ca495..45476336d0 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt @@ -183,13 +183,15 @@ class SftpConnectDialog : DialogFragment() { selectedParsedKeyPairName = requireArguments().getString(ARG_KEYPAIR_NAME) selectPemBTN.text = selectedParsedKeyPairName } - oldPath = SshClientUtils.deriveSftpPathFrom( - requireArguments().getString(ARG_ADDRESS)!!, - requireArguments().getInt(ARG_PORT), - requireArguments().getString(ARG_DEFAULT_PATH, ""), - requireArguments().getString(ARG_USERNAME)!!, - requireArguments().getString(ARG_PASSWORD), - selectedParsedKeyPair + oldPath = SshClientUtils.encryptSshPathAsNecessary( + SshClientUtils.deriveSftpPathFrom( + requireArguments().getString(ARG_ADDRESS)!!, + requireArguments().getInt(ARG_PORT), + requireArguments().getString(ARG_DEFAULT_PATH, ""), + requireArguments().getString(ARG_USERNAME)!!, + requireArguments().getString(ARG_PASSWORD), + selectedParsedKeyPair + ) ) } } @@ -201,13 +203,15 @@ class SftpConnectDialog : DialogFragment() { dialogBuilder .negativeText(R.string.delete) .onNegative { dialog: MaterialDialog, _: DialogAction? -> - val path = SshClientUtils.deriveSftpPathFrom( - hostname, - port, - defaultPath, - username, - requireArguments().getString(ARG_PASSWORD, null), - selectedParsedKeyPair + val path = SshClientUtils.encryptSshPathAsNecessary( + SshClientUtils.deriveSftpPathFrom( + hostname, + port, + defaultPath, + username, + requireArguments().getString(ARG_PASSWORD, null), + selectedParsedKeyPair + ) ) val i = DataUtils.getInstance().containsServer( arrayOf(connectionName, path) @@ -441,7 +445,11 @@ class SftpConnectDialog : DialogFragment() { selectedParsedKeyPair ) } else { - updateSshConnection(connectionName, hostKeyFingerprint, path, encryptedPath) + updateSshConnection( + connectionName, + hostKeyFingerprint, + encryptedPath + ) } } @@ -498,11 +506,10 @@ class SftpConnectDialog : DialogFragment() { private fun updateSshConnection( connectionName: String, hostKeyFingerprint: String, - path: String, encryptedPath: String ): Boolean { DataUtils.getInstance().removeServer(DataUtils.getInstance().containsServer(oldPath)) - DataUtils.getInstance().addServer(arrayOf(connectionName, path)) + DataUtils.getInstance().addServer(arrayOf(connectionName, encryptedPath)) Collections.sort(DataUtils.getInstance().servers, BookSorter()) (activity as MainActivity).drawer.refreshDrawer() AppConfig.getInstance().runInBackground { diff --git a/app/src/androidTest/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest.java similarity index 100% rename from app/src/androidTest/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest.java rename to app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest.java diff --git a/app/src/androidTest/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest2.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest2.java similarity index 100% rename from app/src/androidTest/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest2.java rename to app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest2.java diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshConnectionPoolTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshConnectionPoolTest.java index 6ea2b3d723..f3b6df8620 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshConnectionPoolTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshConnectionPoolTest.java @@ -186,10 +186,13 @@ public void testGetConnectionWithUrl() throws IOException { SSHClient mock = createSshServer("testuser", validPassword); saveSshConnectionSettings(hostKeyPair, "testuser", validPassword, null); assertNotNull( - SshConnectionPool.INSTANCE.getConnection("ssh://testuser:testpassword@127.0.0.1:22222")); + SshConnectionPool.INSTANCE.getConnection( + SshClientUtils.encryptSshPathAsNecessary( + "ssh://testuser:testpassword@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -223,10 +226,13 @@ public void testGetConnectionWithUrlHavingComplexPassword1() throws IOException SSHClient mock = createSshServer("testuser", validPassword); saveSshConnectionSettings(hostKeyPair, "testuser", validPassword, null); assertNotNull( - SshConnectionPool.INSTANCE.getConnection("ssh://testuser:testP@ssw0rd@127.0.0.1:22222")); + SshConnectionPool.INSTANCE.getConnection( + SshClientUtils.encryptSshPathAsNecessary( + "ssh://testuser:testP@ssw0rd@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -244,10 +250,13 @@ public void testGetConnectionWithUrlHavingComplexPassword2() throws IOException SSHClient mock = createSshServer("testuser", validPassword); saveSshConnectionSettings(hostKeyPair, "testuser", validPassword, null); assertNotNull( - SshConnectionPool.INSTANCE.getConnection("ssh://testuser:testP@##word@127.0.0.1:22222")); + SshConnectionPool.INSTANCE.getConnection( + SshClientUtils.encryptSshPathAsNecessary( + "ssh://testuser:testP@##word@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -265,10 +274,13 @@ public void testGetConnectionWithUrlHavingComplexCredential1() throws IOExceptio SSHClient mock = createSshServer("testuser", validPassword); saveSshConnectionSettings(hostKeyPair, "testuser", validPassword, null); assertNotNull( - SshConnectionPool.INSTANCE.getConnection("ssh://testuser:testP@##word@127.0.0.1:22222")); + SshConnectionPool.INSTANCE.getConnection( + SshClientUtils.encryptSshPathAsNecessary( + "ssh://testuser:testP@##word@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -286,10 +298,13 @@ public void testGetConnectionWithUrlHavingComplexCredential2() throws IOExceptio SSHClient mock = createSshServer("testuser", validPassword); saveSshConnectionSettings(hostKeyPair, "testuser", validPassword, null); assertNotNull( - SshConnectionPool.INSTANCE.getConnection("ssh://testuser:testP@##word@127.0.0.1:22222")); + SshConnectionPool.INSTANCE.getConnection( + SshClientUtils.encryptSshPathAsNecessary( + "ssh://testuser:testP@##word@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -309,10 +324,12 @@ public void testGetConnectionWithUrlHavingComplexCredential3() throws IOExceptio saveSshConnectionSettings(hostKeyPair, validUsername, validPassword, null); assertNotNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://test@example.com:testP@ssw0rd@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://test@example.com:testP@ssw0rd@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -332,10 +349,12 @@ public void testGetConnectionWithUrlHavingComplexCredential4() throws IOExceptio saveSshConnectionSettings(hostKeyPair, validUsername, validPassword, null); assertNotNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://test@example.com:testP@ssw0##$@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://test@example.com:testP@ssw0##$@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -355,10 +374,12 @@ public void testGetConnectionWithUrlHavingMinusSignInPassword1() throws IOExcept saveSshConnectionSettings(hostKeyPair, validUsername, validPassword, null); assertNotNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://test@example.com:abcd-efgh@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://test@example.com:abcd-efgh@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -378,10 +399,12 @@ public void testGetConnectionWithUrlHavingMinusSignInPassword2() throws IOExcept saveSshConnectionSettings(hostKeyPair, validUsername, validPassword, null); assertNotNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://test@example.com:---------------@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://test@example.com:---------------@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -401,10 +424,12 @@ public void testGetConnectionWithUrlHavingMinusSignInPassword3() throws IOExcept saveSshConnectionSettings(hostKeyPair, validUsername, validPassword, null); assertNotNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://test@example.com:--agdiuhdpost15@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://test@example.com:--agdiuhdpost15@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); @@ -424,10 +449,12 @@ public void testGetConnectionWithUrlHavingMinusSignInPassword4() throws IOExcept saveSshConnectionSettings(hostKeyPair, validUsername, validPassword, null); assertNotNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://test@example.com:t-h-i-s-i-s-p-a-s-s-w-o-r-d-@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://test@example.com:t-h-i-s-i-s-p-a-s-s-w-o-r-d-@127.0.0.1:22222"))); assertNull( SshConnectionPool.INSTANCE.getConnection( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222")); + SshClientUtils.encryptSshPathAsNecessary( + "ssh://invaliduser:invalidpassword@127.0.0.1:22222"))); verify(mock, atLeastOnce()) .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.getPublic())); diff --git a/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.java b/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.java deleted file mode 100644 index 3c455db9a2..0000000000 --- a/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2014-2020 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.ssh; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -import com.amaze.filemanager.filesystem.ssh.SshClientUtils; - -public class SshClientUtilTest { - @Test - public void testExtractRemotePathFromUri() { - assertEquals( - "/home/user/foo/bar", - SshClientUtils.extractRemotePathFrom("ssh://user:password@127.0.0.1:22/home/user/foo/bar")); - assertEquals("/", SshClientUtils.extractRemotePathFrom("ssh://user:password@127.0.0.1:22/")); - assertEquals("/", SshClientUtils.extractRemotePathFrom("ssh://user:password@127.0.0.1:22")); - } -} diff --git a/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.kt b/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.kt new file mode 100644 index 0000000000..6f0fc5148c --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2014-2020 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.ssh + +import com.amaze.filemanager.filesystem.ssh.SshClientUtils +import org.junit.Assert +import org.junit.Test + +class SshClientUtilTest { + /** + * Test [SshClientUtils.extractRemotePathFrom]. + */ + @Test + fun testExtractRemotePathFromUri() { + Assert.assertEquals( + "/home/user/foo/bar", + SshClientUtils.extractRemotePathFrom( + "ssh://user:password@127.0.0.1:22/home/user/foo/bar" + ) + ) + Assert.assertEquals( + "/", + SshClientUtils.extractRemotePathFrom("ssh://user:password@127.0.0.1:22/") + ) + Assert.assertEquals( + "/", + SshClientUtils.extractRemotePathFrom("ssh://user:password@127.0.0.1:22") + ) + Assert.assertEquals( + "/", SshClientUtils.extractRemotePathFrom("ssh://root:a8/875dbc-==@127.0.0.1:9899") + ) + Assert.assertEquals( + "/root/.config", + SshClientUtils.extractRemotePathFrom( + "ssh://root:a8/875dbc-==@127.0.0.1:9899/root/.config" + ) + ) + } + + /** + * Test [SshClientUtils.extractRemotePathFrom]. + */ + @Test + fun testExtractBaseUriFromUri() { + Assert.assertEquals( + "ssh://root@127.0.0.1", + SshClientUtils.extractBaseUriFrom("ssh://root@127.0.0.1") + ) + Assert.assertEquals( + "ssh://root@127.0.0.1:2233", + SshClientUtils.extractBaseUriFrom("ssh://root@127.0.0.1:2233") + ) + Assert.assertEquals( + "ssh://root@127.0.0.1", + SshClientUtils.extractBaseUriFrom("ssh://root@127.0.0.1/root/.config") + ) + Assert.assertEquals( + "ssh://root:password@127.0.0.1", + SshClientUtils.extractBaseUriFrom("ssh://root:password@127.0.0.1") + ) + Assert.assertEquals( + "ssh://root:password@127.0.0.1:3456", + SshClientUtils.extractBaseUriFrom("ssh://root:password@127.0.0.1:3456/root/.config") + ) + Assert.assertEquals( + "ssh://root:a8/875dbc-==@127.0.0.1:9899", + SshClientUtils.extractBaseUriFrom("ssh://root:a8/875dbc-==@127.0.0.1:9899") + ) + Assert.assertEquals( + "ssh://root:a8/875dbc-==@127.0.0.1:9899", + SshClientUtils.extractBaseUriFrom( + "ssh://root:a8/875dbc-==@127.0.0.1:9899/root/.config" + ) + ) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/test/ShadowCryptUtilTest.java b/app/src/test/java/com/amaze/filemanager/test/ShadowCryptUtilTest.java index 9b8bdb76c4..710b82c9e5 100644 --- a/app/src/test/java/com/amaze/filemanager/test/ShadowCryptUtilTest.java +++ b/app/src/test/java/com/amaze/filemanager/test/ShadowCryptUtilTest.java @@ -102,7 +102,9 @@ public void testWithUtilsHandler() { .atMost(10, TimeUnit.SECONDS) .until( () -> { - assertEquals(fingerprint, utilsHandler.getSshHostKey(url)); + assertEquals( + fingerprint, + utilsHandler.getSshHostKey(SshClientUtils.encryptSshPathAsNecessary(url))); utilitiesDatabase.close(); return true; }); diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.java b/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.java index 8333327804..bf77f4aadc 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.java @@ -28,6 +28,12 @@ import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import java.io.IOException; @@ -39,19 +45,26 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.MockedConstruction; import org.robolectric.annotation.Config; import org.robolectric.annotation.LooperMode; import org.robolectric.shadows.ShadowLooper; +import org.robolectric.shadows.ShadowSQLiteConnection; import org.robolectric.shadows.ShadowStorageManager; +import org.robolectric.util.ReflectionHelpers; import com.amaze.filemanager.application.AppConfig; +import com.amaze.filemanager.database.UtilsHandler; +import com.amaze.filemanager.filesystem.ssh.SshClientUtils; import com.amaze.filemanager.shadows.ShadowMultiDex; import com.amaze.filemanager.shadows.jcifs.smb.ShadowSmbFile; import com.amaze.filemanager.test.ShadowCryptUtil; import com.amaze.filemanager.test.TestUtils; +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog; import com.amaze.filemanager.utils.SmbUtil; import android.os.Build; +import android.os.Bundle; import android.os.storage.StorageManager; import androidx.lifecycle.Lifecycle; @@ -80,6 +93,12 @@ @LooperMode(LooperMode.Mode.PAUSED) public class MainActivityTest { + private static final String[] BUNDLE_KEYS = { + "address", "port", "keypairName", "name", "username", "password", "edit" + }; + + private MockedConstruction mc; + @Before public void setUp() { if (Build.VERSION.SDK_INT >= N) TestUtils.initializeInternalStorage(); @@ -87,6 +106,15 @@ public void setUp() { RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline()); RxAndroidPlugins.reset(); RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline()); + ShadowSQLiteConnection.reset(); + + mc = + mockConstruction( + SftpConnectDialog.class, + (mock, context) -> { + doCallRealMethod().when(mock).setArguments(any()); + when(mock.getArguments()).thenCallRealMethod(); + }); } @After @@ -94,6 +122,56 @@ public void tearDown() { if (Build.VERSION.SDK_INT >= N) shadowOf(ApplicationProvider.getApplicationContext().getSystemService(StorageManager.class)) .resetStorageVolumeList(); + + mc.close(); + } + + @Test + public void testInvokeSftpConnectionDialog() { + + Bundle verify = new Bundle(); + verify.putString("address", "127.0.0.1"); + verify.putInt("port", 22); + verify.putString("name", "SCP/SFTP Connection"); + verify.putString("username", "root"); + verify.putBoolean("hasPassword", false); + verify.putBoolean("edit", true); + verify.putString("keypairName", "abcdefgh"); + + testOpenSftpConnectDialog("ssh://root@127.0.0.1:22", verify); + } + + @Test + public void testInvokeSftpConnectionDialogWithPassword() + throws GeneralSecurityException, IOException { + String uri = "ssh://root:12345678@127.0.0.1:22"; + + Bundle verify = new Bundle(); + verify.putString("address", "127.0.0.1"); + verify.putInt("port", 22); + verify.putString("name", "SCP/SFTP Connection"); + verify.putString("username", "root"); + verify.putBoolean("hasPassword", true); + verify.putBoolean("edit", true); + verify.putString("password", "12345678"); + + testOpenSftpConnectDialog(uri, verify); + } + + private void testOpenSftpConnectDialog(String uri, Bundle verify) { + MainActivity activity = mock(MainActivity.class); + UtilsHandler utilsHandler = mock(UtilsHandler.class); + when(utilsHandler.getSshAuthPrivateKeyName("ssh://root@127.0.0.1:22")).thenReturn("abcdefgh"); + ReflectionHelpers.setField(activity, "utilsHandler", utilsHandler); + doCallRealMethod().when(activity).showSftpDialog(any(), any(), anyBoolean()); + + activity.showSftpDialog( + "SCP/SFTP Connection", SshClientUtils.encryptSshPathAsNecessary(uri), true); + assertEquals(1, mc.constructed().size()); + SftpConnectDialog mocked = mc.constructed().get(0); + for (String key : BUNDLE_KEYS) { + assertEquals(verify.get(key), mocked.getArguments().get(key)); + } } @Test diff --git a/build.gradle b/build.gradle index eb304b2ecf..271c0b5e7e 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { androidXTestExtVersion = "1.1.2" junitVersion = "4.13.2" slf4jVersion = "1.7.25" - mockitoVersion = "3.4.4" + mockitoVersion = "3.9.0" androidBillingVersion = "2.1.0" junrarVersion = "7.4.0" zip4jVersion = "2.6.4" From 8f81c2031014e4447d5d4721dc85ce5ff26c51fe Mon Sep 17 00:00:00 2001 From: EmmanuelMess Date: Sun, 17 Oct 2021 12:30:13 -0300 Subject: [PATCH 05/26] Add C support and move isDirectory to C code --- .gitignore | 274 ++++++++++++++++++ .../filemanager/filesystem/HybridFile.java | 15 +- .../filemanager/filesystem/RootHelper.java | 40 --- .../filesystem/root/ListFilesCommand.kt | 3 +- file_operations/build.gradle | 13 + file_operations/src/main/c/CMakeLists.txt | 12 + file_operations/src/main/c/rootoperations.c | 29 ++ .../filesystem/root/NativeOperations.kt | 11 + 8 files changed, 344 insertions(+), 53 deletions(-) create mode 100644 file_operations/src/main/c/CMakeLists.txt create mode 100644 file_operations/src/main/c/rootoperations.c create mode 100644 file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/root/NativeOperations.kt diff --git a/.gitignore b/.gitignore index 42f583f769..13e44ecdb0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,277 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/android,androidstudio,c +# Edit at https://www.toptal.com/developers/gitignore?templates=android,androidstudio,c + +### Android ### +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/jarRepositories.xml +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +### Android Patch ### +gen-external-apklibs +output.json + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. + +### C ### +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files + +# Files for the ART/Dalvik VM + +# Java class files + +# Generated files + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +*.ipr +*~ +*.swp + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/scopes/scope_settings.xml +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,c + + # Windows image file caches Thumbs.db ehthumbs.db 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 f50bec23f7..ee848d1922 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -41,6 +41,7 @@ import com.amaze.filemanager.file_operations.exceptions.CloudPluginException; import com.amaze.filemanager.file_operations.exceptions.ShellNotRunningException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.root.NativeOperations; import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.root.DeleteFileCommand; @@ -485,12 +486,7 @@ public boolean isDirectory() { isDirectory = getFile().isDirectory(); break; case ROOT: - try { - isDirectory = RootHelper.isDirectory(path, 5); - } catch (ShellNotRunningException e) { - e.printStackTrace(); - isDirectory = false; - } + isDirectory = NativeOperations.isDirectory(path); break; case DOCUMENT_FILE: return getDocumentFile(false).isDirectory(); @@ -552,12 +548,7 @@ public Boolean execute(SFTPClient client) { isDirectory = getFile().isDirectory(); break; case ROOT: - try { - isDirectory = RootHelper.isDirectory(path, 5); - } catch (ShellNotRunningException e) { - e.printStackTrace(); - isDirectory = false; - } + isDirectory = NativeOperations.isDirectory(path); break; case DOCUMENT_FILE: isDirectory = getDocumentFile(false).isDirectory(); diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/RootHelper.java b/app/src/main/java/com/amaze/filemanager/filesystem/RootHelper.java index 5e9255447a..a7318eca64 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/RootHelper.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/RootHelper.java @@ -24,11 +24,8 @@ import java.util.ArrayList; import java.util.List; -import com.amaze.filemanager.file_operations.exceptions.ShellNotRunningException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; -import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.root.ListFilesCommand; -import com.amaze.filemanager.utils.Utils; import androidx.documentfile.provider.DocumentFile; @@ -115,43 +112,6 @@ public static boolean fileExists(String path) { return false; } - /** Whether toTest file is directory or not */ - public static boolean isDirectory(String toTest, int count) throws ShellNotRunningException { - File file = new File(toTest); - String name = file.getName(); - String parentPath = file.getParent(); - if (!Utils.isNullOrEmpty(parentPath)) { - List resultLines = - ListFilesCommand.INSTANCE - .executeRootCommand(getCommandLineString(parentPath), true, false) - .getFirst(); - for (String currentLine : resultLines) { - if (contains(currentLine.split(" "), name)) { - try { - HybridFileParcelable parsedFile = FileUtils.parseName(currentLine, true); - if (parsedFile.getPermission().trim().startsWith("d")) return true; - else if (parsedFile.getPermission().trim().startsWith("l")) { - if (count > 5) return file.isDirectory(); - else return isDirectory(parsedFile.getLink().trim(), count + 1); - } else return file.isDirectory(); - } catch (Exception e) { - e.printStackTrace(); - } - break; - } - } - } - return file.isDirectory(); - } - - static boolean contains(String[] a, String name) { - for (String s : a) { - // Log.e("checking",s); - if (s.equals(name)) return true; - } - return false; - } - /** * Get a list of files using shell, supposing the path is not a SMB/OTG/Custom (*.apk/images) * TODO: Avoid parsing ls diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/root/ListFilesCommand.kt b/app/src/main/java/com/amaze/filemanager/filesystem/root/ListFilesCommand.kt index 18f18983bc..9e7ca4f3c1 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/root/ListFilesCommand.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/root/ListFilesCommand.kt @@ -27,6 +27,7 @@ import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.exceptions.ShellCommandInvalidException import com.amaze.filemanager.file_operations.exceptions.ShellNotRunningException import com.amaze.filemanager.file_operations.filesystem.OpenMode +import com.amaze.filemanager.file_operations.filesystem.root.NativeOperations import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.filesystem.RootHelper import com.amaze.filemanager.filesystem.files.FileUtils @@ -230,7 +231,7 @@ object ListFilesCommand : IRootCommand() { } } } else { - RootHelper.isDirectory(this.link, 0).let { + NativeOperations.isDirectory(this.link).let { this.isDirectory = it } } diff --git a/file_operations/build.gradle b/file_operations/build.gradle index 27b24eab47..9587ae7c80 100644 --- a/file_operations/build.gradle +++ b/file_operations/build.gradle @@ -13,6 +13,12 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" + + externalNativeBuild { + cmake { + cFlags '-std=c11' + } + } } buildTypes { @@ -33,6 +39,13 @@ android { returnDefaultValues = true } } + + externalNativeBuild { + cmake { + path file('src/main/c/CMakeLists.txt') + version '3.10.2' + } + } } dependencies { diff --git a/file_operations/src/main/c/CMakeLists.txt b/file_operations/src/main/c/CMakeLists.txt new file mode 100644 index 0000000000..2ce2b13b1f --- /dev/null +++ b/file_operations/src/main/c/CMakeLists.txt @@ -0,0 +1,12 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +cmake_minimum_required(VERSION 3.10.2) + +project("rootoperations") + +add_library(rootoperations SHARED rootoperations.c) + +find_library(log-lib log) + +target_link_libraries(rootoperations ${log-lib}) diff --git a/file_operations/src/main/c/rootoperations.c b/file_operations/src/main/c/rootoperations.c new file mode 100644 index 0000000000..df3f7621cb --- /dev/null +++ b/file_operations/src/main/c/rootoperations.c @@ -0,0 +1,29 @@ +#include +#include +#include +#include +#include +#include + +JNIEXPORT jboolean JNICALL Java_com_amaze_filemanager_file_1operations_filesystem_root_NativeOperations_isDirectory( + JNIEnv * env, + jobject thiz, + jstring path + ) { + struct stat path_stat; + const char * cPath = (*env)->GetStringUTFChars(env, path, NULL); + int returnCode = stat(cPath, &path_stat); + (*env)->ReleaseStringUTFChars(env, path, cPath); + + if(returnCode == -1) { + switch (errno) { + case ELOOP: + return true; + default: + return false; + } + } + + //This follows links + return S_ISDIR(path_stat.st_mode); +} \ No newline at end of file diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/root/NativeOperations.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/root/NativeOperations.kt new file mode 100644 index 0000000000..7759463044 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/root/NativeOperations.kt @@ -0,0 +1,11 @@ +package com.amaze.filemanager.file_operations.filesystem.root + +object NativeOperations { + init { + System.loadLibrary("rootoperations") + } + + /** Whether path file is directory or not */ + @JvmStatic + external fun isDirectory(path: String?): Boolean +} \ No newline at end of file From fe102790c218eaa68e6388d34dbaf8216478389c Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 19 Oct 2021 17:17:19 +0530 Subject: [PATCH 06/26] Fix zip issue --- .../filemanager/adapters/RecyclerAdapter.java | 30 +++++++++---------- .../com/amaze/filemanager/ui/icons/Icons.java | 3 +- 2 files changed, 17 insertions(+), 16 deletions(-) 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 1df25d42b1..3a3edaea70 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -1287,22 +1287,22 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl } } else { popupMenu.getMenu().findItem(R.id.book).setVisible(false); - } - if (description.endsWith(fileExtensionZip) - || description.endsWith(fileExtensionJar) - || description.endsWith(fileExtensionApk) - || description.endsWith(fileExtensionApks) - || description.endsWith(fileExtensionRar) - || description.endsWith(fileExtensionTar) - || description.endsWith(fileExtensionGzipTarLong) - || description.endsWith(fileExtensionGzipTarShort) - || description.endsWith(fileExtensionBzip2TarLong) - || description.endsWith(fileExtensionBzip2TarShort) - || description.endsWith(fileExtensionXz) - || description.endsWith(fileExtensionLzma) - || description.endsWith(fileExtension7zip)) - popupMenu.getMenu().findItem(R.id.ex).setVisible(true); + if (description.endsWith(fileExtensionZip) + || description.endsWith(fileExtensionJar) + || description.endsWith(fileExtensionApk) + || description.endsWith(fileExtensionApks) + || description.endsWith(fileExtensionRar) + || description.endsWith(fileExtensionTar) + || description.endsWith(fileExtensionGzipTarLong) + || description.endsWith(fileExtensionGzipTarShort) + || description.endsWith(fileExtensionBzip2TarLong) + || description.endsWith(fileExtensionBzip2TarShort) + || description.endsWith(fileExtensionXz) + || description.endsWith(fileExtensionLzma) + || description.endsWith(fileExtension7zip)) + popupMenu.getMenu().findItem(R.id.ex).setVisible(true); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { if (description.endsWith(CryptUtil.CRYPT_EXTENSION)) diff --git a/app/src/main/java/com/amaze/filemanager/ui/icons/Icons.java b/app/src/main/java/com/amaze/filemanager/ui/icons/Icons.java index a0e41864b8..4628a257bb 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/icons/Icons.java +++ b/app/src/main/java/com/amaze/filemanager/ui/icons/Icons.java @@ -197,7 +197,8 @@ private static void putKeys(int resId, String... mimeTypes) { public static @DrawableRes int loadMimeIcon(String path, boolean isDirectory) { if (path.equals("..")) return R.drawable.ic_arrow_left_white_24dp; - if (CompressedHelper.isFileExtractable(path)) return R.drawable.ic_compressed_white_24dp; + if (CompressedHelper.isFileExtractable(path) && !isDirectory) + return R.drawable.ic_compressed_white_24dp; int type = getTypeOfFile(path, isDirectory); From aec63736f9a2642c97884087f35674cf1ab1a5f2 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 19 Oct 2021 17:53:49 +0530 Subject: [PATCH 07/26] Fix arabic string issue --- app/src/main/res/values-ar/strings.xml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 5b8aed9ef4..eedd726cb8 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -322,42 +322,43 @@ -

كيفية التوصل الى حافظة Windows المشتركة في Android (SMB)

\n \n +

كيفية التوصل الى حافظة Windows المشتركة في Android (SMB)

\n \n
  • - اتاحة مشاركة الملف على Windows \n + اتاحة مشاركة الملف على Windows \n
  • - قم بفتح لوحة التحكم ، واضغط اختيار HomeGroup واختيارات المشاركة تحت شبكة الاتصالات والانترنت ، واضغط \\"تغيير محددات المشاركة المتقدمة\" ، واتاحة \"ملف ومشاركة وحدة الطباعة\" الخاصية المميزة. \n \n + قم بفتح لوحة التحكم ، واضغط اختيار HomeGroup واختيارات المشاركة تحت شبكة الاتصالات والانترنت ، واضغط \"تغيير محددات المشاركة المتقدمة\" ، واتاحة \"ملف ومشاركة وحدة الطباعة\" الخاصية المميزة. \n \n

  • - محددات مشاركة الملف الاضافية \n + محددات مشاركة الملف الاضافية \n
  • - قد تريد أيضا توصيف محددات المشاركة المتقدمة الأخرى هنا. \n على سبيل المثال ، يمكنك اتاحة امكانية الاتصال بالملفات الخاصة بك بدون كلمة سرية اذا كنت تثق بكل الأجهزة الموجودة على شبكة الاتصالات المحلية الخاصة بك. بمجرد اتاحة مشاركة الملف ووحدة الطباعة ، يمكنك فتح File Explorer أو Windows Explorer ، ثم اضغط بمفتاح الفأرة الأيمن على الحافظة التي تريد مشاركتها ، وحدد الاختيار خواص. \n اضغط على الاختيار مشاركة واجعل الحافظة متاحة على شبكة الاتصال. \n \n + قد تريد أيضا توصيف محددات المشاركة المتقدمة الأخرى هنا. \n على سبيل المثال ، يمكنك اتاحة امكانية الاتصال بالملفات الخاصة بك بدون كلمة سرية اذا كنت تثق بكل الأجهزة الموجودة على شبكة الاتصالات المحلية الخاصة بك. بمجرد اتاحة مشاركة الملف ووحدة الطباعة ، يمكنك فتح File Explorer أو Windows Explorer ، ثم اضغط بمفتاح الفأرة الأيمن على الحافظة التي تريد مشاركتها ، وحدد الاختيار خواص. \n اضغط على الاختيار مشاركة واجعل الحافظة متاحة على شبكة الاتصال. \n \n

  • - تأكد من أن كلا من الأجهزة على نفس Wifi \n + تأكد من أن كلا من الأجهزة على نفس Wifi \n
  • - تقوم هذه الخاصية باتاحة الملفات الموجودة على شبكة الاتصالات المحلية ، ولذلك يجب أن يكون الحاسب الشخصي والأجهزة النقالة على نفس شبكة الاتصالات المحلية. لا يمكنك الاتصال بحافظة Windows المشتركة عبر شبكة الانترنت أو عند توصيل جهاز التليفون الذكي الخاص بك ببيانات الجهاز النقال الخاص به-يجب توصيله الى Wi-Fi. \n \n + تقوم هذه الخاصية باتاحة الملفات الموجودة على شبكة الاتصالات المحلية ، ولذلك يجب أن يكون الحاسب الشخصي والأجهزة النقالة على نفس شبكة الاتصالات المحلية. لا يمكنك الاتصال بحافظة Windows المشتركة عبر شبكة الانترنت أو عند توصيل جهاز التليفون الذكي الخاص بك ببيانات الجهاز النقال الخاص به-يجب توصيله الى Wi-Fi. \n \n

  • - ايجاد عنوان IP \n + ايجاد عنوان IP \n
  • - فتح رسالة حث الأمر. أدخل \'\ ipconfig \'\ ثم اضغط Enter. ابحث عن Default Gateway تحت موفق شبكة الاتصالات الخاص بك لتوجيه مسار التوجيه الخاص بك \\' عنوان IP. أنظر الى \\"عنوان IPv4 \"\ تحت نفس قسم الموفق لايجاد IP الخاص بالحاسب الخاص بك - عنوان. \n \n + فتح رسالة حث الأمر. أدخل \'\ ipconfig \'\ ثم اضغط Enter. ابحث عن Default Gateway تحت موفق شبكة الاتصالات الخاص بك لتوجيه مسار التوجيه الخاص بك \' عنوان IP. أنظر الى \"عنوان IPv4 \"\ تحت نفس قسم الموفق لايجاد IP الخاص بالحاسب الخاص بك + عنوان. \n \n

    - أدخل التفاصيل في مربع حوار SMB \n + أدخل التفاصيل في مربع حوار SMB \n
+ لا يمكن مشاركة أكثر من 100 ملف حدد الدليل الرئيسي لجهاز OTG المعلومات @@ -566,7 +567,7 @@ تعديل وصلة SCP/SFTP المنفذ - لا يمكن تحديد صحة النظام الرئيسي \\'%1$s \'\. \n\n %2$s بصمة المفتاح هي %3$s. \n \npth \"\ نعم \\"لتأكيد الهوية ؛ خلاف ذلك ، برجاء النقر \\" لا \"\. + لا يمكن تحديد صحة النظام الرئيسي \\\'%1$s \'\. \n\n %2$s بصمة المفتاح هي %3$s. \n \npth \"\ نعم \\"لتأكيد الهوية ؛ خلاف ذلك ، برجاء النقر \\" لا \"\. التحقق من النظام الرئيسي مفتاح SSH الخاص غير قادر على قراءة PEM المحدد : %s From f26ddb81318e476aee06bad626c70187029a297c Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 19 Oct 2021 20:46:50 +0530 Subject: [PATCH 08/26] Revert "Fix arabic string issue" This reverts commit aec63736 --- app/src/main/res/values-ar/strings.xml | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index eedd726cb8..5b8aed9ef4 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -322,43 +322,42 @@ -

كيفية التوصل الى حافظة Windows المشتركة في Android (SMB)

\n \n +

كيفية التوصل الى حافظة Windows المشتركة في Android (SMB)

\n \n
  • - اتاحة مشاركة الملف على Windows \n + اتاحة مشاركة الملف على Windows \n
  • - قم بفتح لوحة التحكم ، واضغط اختيار HomeGroup واختيارات المشاركة تحت شبكة الاتصالات والانترنت ، واضغط \"تغيير محددات المشاركة المتقدمة\" ، واتاحة \"ملف ومشاركة وحدة الطباعة\" الخاصية المميزة. \n \n + قم بفتح لوحة التحكم ، واضغط اختيار HomeGroup واختيارات المشاركة تحت شبكة الاتصالات والانترنت ، واضغط \\"تغيير محددات المشاركة المتقدمة\" ، واتاحة \"ملف ومشاركة وحدة الطباعة\" الخاصية المميزة. \n \n

  • - محددات مشاركة الملف الاضافية \n + محددات مشاركة الملف الاضافية \n
  • - قد تريد أيضا توصيف محددات المشاركة المتقدمة الأخرى هنا. \n على سبيل المثال ، يمكنك اتاحة امكانية الاتصال بالملفات الخاصة بك بدون كلمة سرية اذا كنت تثق بكل الأجهزة الموجودة على شبكة الاتصالات المحلية الخاصة بك. بمجرد اتاحة مشاركة الملف ووحدة الطباعة ، يمكنك فتح File Explorer أو Windows Explorer ، ثم اضغط بمفتاح الفأرة الأيمن على الحافظة التي تريد مشاركتها ، وحدد الاختيار خواص. \n اضغط على الاختيار مشاركة واجعل الحافظة متاحة على شبكة الاتصال. \n \n + قد تريد أيضا توصيف محددات المشاركة المتقدمة الأخرى هنا. \n على سبيل المثال ، يمكنك اتاحة امكانية الاتصال بالملفات الخاصة بك بدون كلمة سرية اذا كنت تثق بكل الأجهزة الموجودة على شبكة الاتصالات المحلية الخاصة بك. بمجرد اتاحة مشاركة الملف ووحدة الطباعة ، يمكنك فتح File Explorer أو Windows Explorer ، ثم اضغط بمفتاح الفأرة الأيمن على الحافظة التي تريد مشاركتها ، وحدد الاختيار خواص. \n اضغط على الاختيار مشاركة واجعل الحافظة متاحة على شبكة الاتصال. \n \n

  • - تأكد من أن كلا من الأجهزة على نفس Wifi \n + تأكد من أن كلا من الأجهزة على نفس Wifi \n
  • - تقوم هذه الخاصية باتاحة الملفات الموجودة على شبكة الاتصالات المحلية ، ولذلك يجب أن يكون الحاسب الشخصي والأجهزة النقالة على نفس شبكة الاتصالات المحلية. لا يمكنك الاتصال بحافظة Windows المشتركة عبر شبكة الانترنت أو عند توصيل جهاز التليفون الذكي الخاص بك ببيانات الجهاز النقال الخاص به-يجب توصيله الى Wi-Fi. \n \n + تقوم هذه الخاصية باتاحة الملفات الموجودة على شبكة الاتصالات المحلية ، ولذلك يجب أن يكون الحاسب الشخصي والأجهزة النقالة على نفس شبكة الاتصالات المحلية. لا يمكنك الاتصال بحافظة Windows المشتركة عبر شبكة الانترنت أو عند توصيل جهاز التليفون الذكي الخاص بك ببيانات الجهاز النقال الخاص به-يجب توصيله الى Wi-Fi. \n \n

  • - ايجاد عنوان IP \n + ايجاد عنوان IP \n
  • - فتح رسالة حث الأمر. أدخل \'\ ipconfig \'\ ثم اضغط Enter. ابحث عن Default Gateway تحت موفق شبكة الاتصالات الخاص بك لتوجيه مسار التوجيه الخاص بك \' عنوان IP. أنظر الى \"عنوان IPv4 \"\ تحت نفس قسم الموفق لايجاد IP الخاص بالحاسب الخاص بك - عنوان. \n \n + فتح رسالة حث الأمر. أدخل \'\ ipconfig \'\ ثم اضغط Enter. ابحث عن Default Gateway تحت موفق شبكة الاتصالات الخاص بك لتوجيه مسار التوجيه الخاص بك \\' عنوان IP. أنظر الى \\"عنوان IPv4 \"\ تحت نفس قسم الموفق لايجاد IP الخاص بالحاسب الخاص بك + عنوان. \n \n

    - أدخل التفاصيل في مربع حوار SMB \n + أدخل التفاصيل في مربع حوار SMB \n
- لا يمكن مشاركة أكثر من 100 ملف حدد الدليل الرئيسي لجهاز OTG المعلومات @@ -567,7 +566,7 @@ تعديل وصلة SCP/SFTP المنفذ - لا يمكن تحديد صحة النظام الرئيسي \\\'%1$s \'\. \n\n %2$s بصمة المفتاح هي %3$s. \n \npth \"\ نعم \\"لتأكيد الهوية ؛ خلاف ذلك ، برجاء النقر \\" لا \"\. + لا يمكن تحديد صحة النظام الرئيسي \\'%1$s \'\. \n\n %2$s بصمة المفتاح هي %3$s. \n \npth \"\ نعم \\"لتأكيد الهوية ؛ خلاف ذلك ، برجاء النقر \\" لا \"\. التحقق من النظام الرئيسي مفتاح SSH الخاص غير قادر على قراءة PEM المحدد : %s From c73961b2db88679e4fda5221abd86d69a42cefe0 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Thu, 21 Oct 2021 03:38:42 +0530 Subject: [PATCH 09/26] Add cxx in .gitignore --- file_operations/.gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/file_operations/.gitignore b/file_operations/.gitignore index 42afabfd2a..ed6456e9a8 100644 --- a/file_operations/.gitignore +++ b/file_operations/.gitignore @@ -1 +1,5 @@ -/build \ No newline at end of file +/build + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ \ No newline at end of file From cc3d81075a8537ba56702473cb40527b91148e53 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Mon, 25 Oct 2021 04:21:53 +0530 Subject: [PATCH 10/26] 2910 Allow main fragment viewmodel to persist adapter items --- .../filemanager/adapters/RecyclerAdapter.java | 183 +++++++++--------- .../ui/fragments/MainFragment.java | 19 +- .../fragments/data/MainFragmentViewModel.kt | 15 ++ 3 files changed, 116 insertions(+), 101 deletions(-) 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 3a3edaea70..cd94a3d676 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -120,7 +120,6 @@ public class RecyclerAdapter extends RecyclerView.Adapter preloader; private RecyclerPreloadSizeProvider sizeProvider; private RecyclerPreloadModelProvider modelProvider; - private ArrayList itemsDigested = new ArrayList<>(); @NonNull private final Context context; private LayoutInflater mInflater; private float minRowHeight; @@ -190,22 +189,22 @@ public RecyclerAdapter( * @param imageView the check {@link CircleGradientDrawable} that is to be animated */ public void toggleChecked(int position, ImageView imageView) { - if (itemsDigested.size() <= position || position < 0) { + if (getItemsDigested().size() <= position || position < 0) { AppConfig.toast(context, R.string.operation_not_supported); return; } - if (itemsDigested.get(position).getChecked() == ListItem.UNCHECKABLE) { + if (getItemsDigested().get(position).getChecked() == ListItem.UNCHECKABLE) { throw new IllegalArgumentException("You have checked a header"); } if (!stoppedAnimation) mainFrag.stopAnimation(); - if (itemsDigested.get(position).getChecked() == ListItem.CHECKED) { + if (getItemsDigested().get(position).getChecked() == ListItem.CHECKED) { // if the view at position is checked, un-check it Log.d( getClass().getSimpleName(), String.format("the view at position %s is checked, un-check it", position)); - itemsDigested.get(position).setChecked(false); + getItemsDigested().get(position).setChecked(false); Animation iconAnimation = AnimationUtils.loadAnimation(context, R.anim.check_out); if (imageView != null) { @@ -219,7 +218,7 @@ public void toggleChecked(int position, ImageView imageView) { Log.d( getClass().getSimpleName(), String.format("the view at position %s is unchecked, check it", position)); - itemsDigested.get(position).setChecked(true); + getItemsDigested().get(position).setChecked(true); Animation iconAnimation = AnimationUtils.loadAnimation(context, R.anim.check_in); if (imageView != null) { @@ -236,32 +235,36 @@ public void toggleChecked(int position, ImageView imageView) { if (mainFrag.getMainFragmentViewModel() != null) { mainFrag.getMainFragmentViewModel().setSelection(true); } - mainFrag.mActionMode = - mainFrag.getMainActivity().startSupportActionMode(mainFrag.mActionModeCallback); } } notifyItemChanged(position); - invalidateActionMode(); } public void invalidateActionMode() { - if (mainFrag.mActionMode != null - && mainFrag.getMainFragmentViewModel() != null - && mainFrag.getMainFragmentViewModel().getSelection()) { + if (mainFrag.getMainFragmentViewModel() != null) { // we have the actionmode visible, invalidate it's views - mainFrag.mActionMode.invalidate(); - } - if (getCheckedItems().size() == 0) { - mainFrag.disableActionMode(); + if (mainFrag.getMainFragmentViewModel().getSelection() && getCheckedItems().size() != 0) { + if (mainFrag.mActionMode == null) { + mainFrag.mActionMode = + mainFrag.getMainActivity().startSupportActionMode(mainFrag.mActionModeCallback); + } else { + mainFrag.mActionMode.invalidate(); + } + } else { + if (mainFrag.mActionMode != null) { + mainFrag.mActionMode.finish(); + mainFrag.mActionMode = null; + } + } } } public void toggleChecked(boolean selectAll, String path) { int i = path.equals("/") || !getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON) ? 0 : 1; - for (; i < itemsDigested.size(); i++) { - ListItem item = itemsDigested.get(i); + for (; i < getItemsDigested().size(); i++) { + ListItem item = getItemsDigested().get(i); if (selectAll && item.getChecked() != ListItem.CHECKED) { item.setChecked(true); notifyItemChanged(i); @@ -270,14 +273,13 @@ public void toggleChecked(boolean selectAll, String path) { notifyItemChanged(i); } } - invalidateActionMode(); } public void toggleInverse(String path) { int i = path.equals("/") || !getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON) ? 0 : 1; - for (; i < itemsDigested.size(); i++) { - ListItem item = itemsDigested.get(i); + for (; i < getItemsDigested().size(); i++) { + ListItem item = getItemsDigested().get(i); if (item.getChecked() != ListItem.CHECKED) { item.setChecked(true); notifyItemChanged(i); @@ -286,16 +288,15 @@ public void toggleInverse(String path) { notifyItemChanged(i); } } - invalidateActionMode(); } public void toggleSameTypes() { ArrayList checkedItemsIndexes = getCheckedItemsIndex(); for (int i = 0; i < checkedItemsIndexes.size(); i++) { LayoutElementParcelable selectedElement = - itemsDigested.get(checkedItemsIndexes.get(i)).getElem(); - for (int z = 0; z < itemsDigested.size(); z++) { - ListItem currentItem = itemsDigested.get(z); + getItemsDigested().get(checkedItemsIndexes.get(i)).getElem(); + for (int z = 0; z < getItemsDigested().size(); z++) { + ListItem currentItem = getItemsDigested().get(z); if (currentItem.getElem() == null) { // header type list item ('Files' / 'Folders') continue; @@ -318,16 +319,15 @@ public void toggleSameTypes() { } } } - invalidateActionMode(); } public void toggleSameDates() { ArrayList checkedItemsIndexes = getCheckedItemsIndex(); for (int i = 0; i < checkedItemsIndexes.size(); i++) { LayoutElementParcelable selectedElement = - itemsDigested.get(checkedItemsIndexes.get(i)).getElem(); - for (int z = 0; z < itemsDigested.size(); z++) { - ListItem currentItem = itemsDigested.get(z); + getItemsDigested().get(checkedItemsIndexes.get(i)).getElem(); + for (int z = 0; z < getItemsDigested().size(); z++) { + ListItem currentItem = getItemsDigested().get(z); if (currentItem.getElem() == null) { // header type list item ('Files' / 'Folders') continue; @@ -341,18 +341,17 @@ public void toggleSameDates() { } } } - invalidateActionMode(); } public void toggleSimilarNames() { ArrayList checkedItemsIndexes = getCheckedItemsIndex(); for (int i = 0; i < checkedItemsIndexes.size(); i++) { LayoutElementParcelable selectedElement = - itemsDigested.get(checkedItemsIndexes.get(i)).getElem(); + getItemsDigested().get(checkedItemsIndexes.get(i)).getElem(); int fuzzinessFactor = selectedElement.title.length() / SelectionPopupMenu.FUZZYNESS_FACTOR; if (fuzzinessFactor >= 1) { - for (int z = 0; z < itemsDigested.size(); z++) { - ListItem currentItem = itemsDigested.get(z); + for (int z = 0; z < getItemsDigested().size(); z++) { + ListItem currentItem = getItemsDigested().get(z); if (currentItem.getElem() == null) { // header type list item ('Files' / 'Folders') continue; @@ -379,7 +378,6 @@ public void toggleSimilarNames() { } } } - invalidateActionMode(); } /** @@ -388,8 +386,8 @@ public void toggleSimilarNames() { * @param b if to toggle true or false */ public void toggleChecked(boolean b) { - for (int i = 0; i < itemsDigested.size(); i++) { - ListItem item = itemsDigested.get(i); + for (int i = 0; i < getItemsDigested().size(); i++) { + ListItem item = getItemsDigested().get(i); if (b && item.getChecked() != ListItem.CHECKED) { item.setChecked(true); notifyItemChanged(i); @@ -398,30 +396,21 @@ public void toggleChecked(boolean b) { notifyItemChanged(i); } } - invalidateActionMode(); } public ArrayList getCheckedItems() { - ArrayList selected = new ArrayList<>(); - - for (int i = 0; i < itemsDigested.size(); i++) { - if (itemsDigested.get(i).getChecked() == ListItem.CHECKED) { - selected.add(itemsDigested.get(i).elem); - } - } - - return selected; + return mainFrag.getMainFragmentViewModel().getCheckedItems(); } public ArrayList getItemsDigested() { - return itemsDigested; + return mainFrag.getMainFragmentViewModel().getAdapterListItems(); } public boolean areAllChecked(String path) { int i = (path.equals("/") || !getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON)) ? 0 : 1; - for (; i < itemsDigested.size(); i++) { - if (itemsDigested.get(i).getChecked() == ListItem.NOT_CHECKED) { + for (; i < getItemsDigested().size(); i++) { + if (getItemsDigested().get(i).getChecked() == ListItem.NOT_CHECKED) { return false; } } @@ -431,8 +420,8 @@ public boolean areAllChecked(String path) { public ArrayList getCheckedItemsIndex() { ArrayList checked = new ArrayList<>(); - for (int i = 0; i < itemsDigested.size(); i++) { - if (itemsDigested.get(i).getChecked() == ListItem.CHECKED) { + for (int i = 0; i < getItemsDigested().size(); i++) { + if (getItemsDigested().get(i).getChecked() == ListItem.CHECKED) { checked.add(i); } } @@ -486,14 +475,14 @@ public void addItem(LayoutElementParcelable e) { // TODO: simplify if condition if (mainFrag.getMainFragmentViewModel() != null && mainFrag.getMainFragmentViewModel().isList() - && itemsDigested.size() > 0) { - itemsDigested.add(itemsDigested.size() - 1, new ListItem(e)); + && getItemsDigested().size() > 0) { + getItemsDigested().add(getItemsDigested().size() - 1, new ListItem(e)); } else if (mainFrag.getMainFragmentViewModel() != null && mainFrag.getMainFragmentViewModel().isList()) { - itemsDigested.add(new ListItem(e)); - itemsDigested.add(new ListItem(EMPTY_LAST_ITEM)); + getItemsDigested().add(new ListItem(e)); + getItemsDigested().add(new ListItem(EMPTY_LAST_ITEM)); } else { - itemsDigested.add(new ListItem(e)); + getItemsDigested().add(new ListItem(e)); } notifyItemInserted(getItemCount()); @@ -512,36 +501,50 @@ private void setItems( preloader = null; } - itemsDigested.clear(); + if (getItemsDigested() != null + && mainFrag.getMainFragmentViewModel().getIconList() != null + && invalidate) { + getItemsDigested().clear(); + mainFrag.getMainFragmentViewModel().getIconList().clear(); + } + offset = 0; stoppedAnimation = false; - ArrayList uris = new ArrayList<>(itemsDigested.size()); + ArrayList uris = new ArrayList<>(); + ArrayList listItems = new ArrayList<>(); for (LayoutElementParcelable e : elements) { - itemsDigested.add(new ListItem(e.isBack, e)); - uris.add(e != null ? e.iconData : null); + if (getItemsDigested() == null || invalidate) { + listItems.add(new ListItem(e.isBack, e)); + uris.add(e != null ? e.iconData : null); + } } if (mainFrag.getMainFragmentViewModel() != null && mainFrag.getMainFragmentViewModel().isList() - && itemsDigested.size() > 0) { - itemsDigested.add(new ListItem(EMPTY_LAST_ITEM)); - uris.add(null); + && listItems.size() > 0) { + if (getItemsDigested() == null || invalidate) { + listItems.add(new ListItem(EMPTY_LAST_ITEM)); + uris.add(null); + } } - for (int i = 0; i < itemsDigested.size(); i++) { - itemsDigested.get(i).setAnimate(false); - } + if (getItemsDigested() == null || invalidate) { + mainFrag.getMainFragmentViewModel().setAdapterListItems(listItems); + mainFrag.getMainFragmentViewModel().setIconList(uris); - if (getBoolean(PREFERENCE_SHOW_HEADERS)) { - createHeaders(invalidate, uris); + if (getBoolean(PREFERENCE_SHOW_HEADERS)) { + createHeaders(invalidate, mainFrag.getMainFragmentViewModel().getIconList()); + } } boolean isItemCircular = !isGrid; sizeProvider = new RecyclerPreloadSizeProvider(this); - modelProvider = new RecyclerPreloadModelProvider(mainFrag, uris, isItemCircular); + modelProvider = + new RecyclerPreloadModelProvider( + mainFrag, mainFrag.getMainFragmentViewModel().getIconList(), isItemCircular); preloader = new RecyclerViewPreloader<>( @@ -553,14 +556,14 @@ private void setItems( public void createHeaders(boolean invalidate, List uris) { boolean[] headers = new boolean[] {false, false}; - for (int i = 0; i < itemsDigested.size(); i++) { + for (int i = 0; i < getItemsDigested().size(); i++) { - if (itemsDigested.get(i).elem != null) { - LayoutElementParcelable nextItem = itemsDigested.get(i).elem; + if (getItemsDigested().get(i).elem != null) { + LayoutElementParcelable nextItem = getItemsDigested().get(i).elem; if (!headers[0] && nextItem.isDirectory) { headers[0] = true; - itemsDigested.add(i, new ListItem(TYPE_HEADER_FOLDERS)); + getItemsDigested().add(i, new ListItem(TYPE_HEADER_FOLDERS)); uris.add(i, null); continue; } @@ -570,7 +573,7 @@ public void createHeaders(boolean invalidate, List uris) { && !nextItem.title.equals(".") && !nextItem.title.equals("..")) { headers[1] = true; - itemsDigested.add(i, new ListItem(TYPE_HEADER_FILES)); + getItemsDigested().add(i, new ListItem(TYPE_HEADER_FILES)); uris.add(i, null); continue; // leave this continue for symmetry } @@ -584,7 +587,7 @@ public void createHeaders(boolean invalidate, List uris) { @Override public int getItemCount() { - return itemsDigested.size(); + return getItemsDigested().size(); } @Override @@ -594,8 +597,8 @@ public long getItemId(int position) { @Override public int getItemViewType(int position) { - if (itemsDigested.get(position).specialType != -1) { - return itemsDigested.get(position).specialType; + if (getItemsDigested().get(position).specialType != -1) { + return getItemsDigested().get(position).specialType; } else { return TYPE_ITEM; } @@ -661,7 +664,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder vholder, int p) { }); holder.txtTitle.setEllipsize( enableMarquee ? TextUtils.TruncateAt.MARQUEE : TextUtils.TruncateAt.MIDDLE); - final boolean isBackButton = itemsDigested.get(p).specialType == TYPE_BACK; + final boolean isBackButton = getItemsDigested().get(p).specialType == TYPE_BACK; if (isBackButton) { holder.about.setVisibility(View.GONE); } @@ -669,7 +672,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder vholder, int p) { && mainFrag.getMainFragmentViewModel().isList()) { if (p == getItemCount() - 1) { holder.rl.setMinimumHeight((int) minRowHeight); - if (itemsDigested.size() == (getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON) ? 1 : 0)) + if (getItemsDigested().size() == (getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON) ? 1 : 0)) holder.txtTitle.setText(R.string.no_files); else { holder.txtTitle.setText(""); @@ -677,11 +680,11 @@ public void onBindViewHolder(final RecyclerView.ViewHolder vholder, int p) { return; } } - if (!this.stoppedAnimation && !itemsDigested.get(p).getAnimating()) { + if (!this.stoppedAnimation && !getItemsDigested().get(p).getAnimating()) { animate(holder); - itemsDigested.get(p).setAnimate(true); + getItemsDigested().get(p).setAnimate(true); } - final LayoutElementParcelable rowItem = itemsDigested.get(p).elem; + final LayoutElementParcelable rowItem = getItemsDigested().get(p).elem; if (dragAndDropPreference != PreferencesConstants.PREFERENCE_DRAG_DEFAULT) { holder.rl.setOnDragListener( new RecyclerAdapterDragListener(this, holder, dragAndDropPreference, mainFrag)); @@ -692,7 +695,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder vholder, int p) { if (!isBackButton) { if (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_DEFAULT || (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_TO_MOVE_COPY - && itemsDigested.get(vholder.getAdapterPosition()).getChecked() + && getItemsDigested().get(vholder.getAdapterPosition()).getChecked() != ListItem.CHECKED)) { toggleChecked( vholder.getAdapterPosition(), @@ -704,6 +707,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder vholder, int p) { } return true; }); + if (mainFrag.getMainFragmentViewModel().isList()) { // clear previously cached icon GlideApp.with(mainFrag).clear(holder.genericIcon); @@ -840,7 +844,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder vholder, int p) { holder.rl.setBackgroundResource(R.drawable.safr_ripple_black); } holder.rl.setSelected(false); - if (itemsDigested.get(p).getChecked() == ListItem.CHECKED) { + if (getItemsDigested().get(p).getChecked() == ListItem.CHECKED) { if (holder.checkImageView.getVisibility() == View.INVISIBLE) holder.checkImageView.setVisibility(View.VISIBLE); @@ -987,7 +991,7 @@ && getBoolean(PREFERENCE_SHOW_THUMB))) { } } - if (itemsDigested.get(p).getChecked() == ListItem.CHECKED) { + if (getItemsDigested().get(p).getChecked() == ListItem.CHECKED) { if (holder.genericIcon.getVisibility() == View.VISIBLE) { if ((rowItem.filetype != Icons.IMAGE @@ -1032,6 +1036,7 @@ && getBoolean(PREFERENCE_SHOW_THUMB))) { } if (getBoolean(PREFERENCE_SHOW_PERMISSIONS)) holder.perm.setText(rowItem.permissions); } + invalidateActionMode(); } } @@ -1040,7 +1045,7 @@ public int getCorrectView(IconDataParcelable item, int adapterPosition) { if (mainFrag.getMainFragmentViewModel() != null && mainFrag.getMainFragmentViewModel().isList()) { if (getBoolean(PREFERENCE_SHOW_THUMB)) { - int filetype = itemsDigested.get(adapterPosition).elem.filetype; + int filetype = getItemsDigested().get(adapterPosition).elem.filetype; if (filetype == Icons.VIDEO || filetype == Icons.IMAGE) { if (getBoolean(PREFERENCE_USE_CIRCULAR_IMAGES)) { @@ -1065,12 +1070,12 @@ public int getCorrectView(IconDataParcelable item, int adapterPosition) { private void initDragListener(int position, View view, ItemViewHolder itemViewHolder) { if (dragAndDropPreference != PreferencesConstants.PREFERENCE_DRAG_DEFAULT - && (itemsDigested.get(position).getChecked() == ListItem.CHECKED + && (getItemsDigested().get(position).getChecked() == ListItem.CHECKED || dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_TO_SELECT)) { // toggle drag flag to true for list item due to the fact // that we might have set it false in a previous drag event - if (!itemsDigested.get(position).shouldToggleDragChecked) { - itemsDigested.get(position).toggleShouldToggleDragChecked(); + if (!getItemsDigested().get(position).shouldToggleDragChecked) { + getItemsDigested().get(position).toggleShouldToggleDragChecked(); } View shadowView = diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java index 3ce1ef58ed..51c19e4d69 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java @@ -379,11 +379,6 @@ private void loadViews() { reloadListElements( true, mainFragmentViewModel.getResults(), !mainFragmentViewModel.isList()); } - if (mainFragmentViewModel.getSelection()) { - for (Integer index : adapter.getCheckedItemsIndex()) { - adapter.toggleChecked(index, null); - } - } } else { loadlist(mainFragmentViewModel.getHome(), true, mainFragmentViewModel.getOpenMode()); } @@ -448,7 +443,7 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { * onCreateActionMode, but may be called multiple times if the mode is invalidated. */ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - ArrayList checkedItems = adapter.getCheckedItems(); + ArrayList checkedItems = mainFragmentViewModel.getCheckedItems(); actionModeView.setOnClickListener( v -> SelectionPopupMenu.Companion.invokeSelectionDropdown( @@ -484,7 +479,7 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { showOption(R.id.openwith, menu); showOption(R.id.share, menu); - if (adapter.getCheckedItems().get(0).isDirectory) { + if (mainFragmentViewModel.getCheckedItems().get(0).isDirectory) { hideOption(R.id.openwith, menu); hideOption(R.id.share, menu); hideOption(R.id.openmulti, menu); @@ -498,7 +493,7 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { showOption(R.id.share, menu); if (getMainActivity().mReturnIntent) if (Build.VERSION.SDK_INT >= 16) showOption(R.id.openmulti, menu); - for (LayoutElementParcelable e : adapter.getCheckedItems()) { + for (LayoutElementParcelable e : mainFragmentViewModel.getCheckedItems()) { if (e.isDirectory) { hideOption(R.id.share, menu); hideOption(R.id.openmulti, menu); @@ -517,7 +512,7 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { showOption(R.id.openwith, menu); showOption(R.id.share, menu); - if (adapter.getCheckedItems().get(0).isDirectory) { + if (mainFragmentViewModel.getCheckedItems().get(0).isDirectory) { hideOption(R.id.openwith, menu); hideOption(R.id.share, menu); hideOption(R.id.openmulti, menu); @@ -533,7 +528,7 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { if (getMainActivity().mReturnIntent) if (Build.VERSION.SDK_INT >= 16) showOption(R.id.openmulti, menu); try { - for (LayoutElementParcelable e : adapter.getCheckedItems()) { + for (LayoutElementParcelable e : mainFragmentViewModel.getCheckedItems()) { if (e.isDirectory) { hideOption(R.id.share, menu); hideOption(R.id.openmulti, menu); @@ -559,7 +554,7 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // called when the user selects a contextual menu item public boolean onActionItemClicked(ActionMode mode, MenuItem item) { computeScroll(); - ArrayList checkedItems = adapter.getCheckedItems(); + ArrayList checkedItems = mainFragmentViewModel.getCheckedItems(); switch (item.getItemId()) { case R.id.openmulti: try { @@ -1957,7 +1952,7 @@ public void smoothScrollListView(boolean upDirection) { if (upDirection) { listView.smoothScrollToPosition(0); } else { - listView.smoothScrollToPosition(adapter.getItemsDigested().size()); + listView.smoothScrollToPosition(mainFragmentViewModel.getAdapterListItems().size()); } } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt index 383b0fe4d9..9b82d4089b 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt @@ -23,6 +23,8 @@ package com.amaze.filemanager.ui.fragments.data import android.content.SharedPreferences import android.os.Bundle import androidx.lifecycle.ViewModel +import com.amaze.filemanager.adapters.RecyclerAdapter +import com.amaze.filemanager.adapters.data.IconDataParcelable import com.amaze.filemanager.adapters.data.LayoutElementParcelable import com.amaze.filemanager.database.CloudHandler import com.amaze.filemanager.file_operations.filesystem.OpenMode @@ -38,6 +40,9 @@ class MainFragmentViewModel : ViewModel() { /** This is not an exact copy of the elements in the adapter */ var listElements: ArrayList? = null + var adapterListItems: ArrayList? = null + var iconList: ArrayList? = null + var fileCount = 0 var folderCount: Int = 0 var columns: Int = 0 @@ -191,4 +196,14 @@ class MainFragmentViewModel : ViewModel() { CloudHandler.CLOUD_PREFIX_BOX + "/" == currentPath || CloudHandler.CLOUD_PREFIX_DROPBOX + "/" == currentPath } + + fun getCheckedItems(): ArrayList { + val selected = ArrayList() + for (i in adapterListItems!!.indices) { + if (adapterListItems!![i].checked == RecyclerAdapter.ListItem.CHECKED) { + selected.add(adapterListItems!![i].elem) + } + } + return selected + } } From 687c04acd6d6099a9286b1d90b1289d73248aee7 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Wed, 27 Oct 2021 23:04:01 +0800 Subject: [PATCH 11/26] Fix Sign-in Google Button won't go away after creating Google Drive connection Fix problem brought by #2832. Previously, after tapping the Google Drive selection, it did brought up the Cloud Plugin dialog, but did nothing afterwards, and the Sign in with Google dialog kept on screen. This changeset fixes this behaviour, by in align with behaviours of buttons of other cloud connection types. --- .../ui/dialogs/GeneralDialogCreation.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java index 3c4247fc95..ab62b5dc5b 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java @@ -1433,19 +1433,24 @@ public static void showSignInWithGoogleDialog(@NonNull MainActivity mainActivity View customView = DialogSigninWithGoogleBinding.inflate(LayoutInflater.from(mainActivity)).getRoot(); int accentColor = mainActivity.getAccent(); + + MaterialDialog dialog = + new MaterialDialog.Builder(mainActivity) + .customView(customView, false) + .title(R.string.signin_with_google_title) + .negativeText(android.R.string.cancel) + .negativeColor(accentColor) + .onNegative((dlg, which) -> dlg.dismiss()) + .build(); + customView .findViewById(R.id.signin_with_google) .setOnClickListener( v -> { mainActivity.addConnection(OpenMode.GDRIVE); + dialog.dismiss(); }); - new MaterialDialog.Builder(mainActivity) - .customView(customView, false) - .title(R.string.signin_with_google_title) - .negativeText(android.R.string.cancel) - .negativeColor(accentColor) - .onNegative((dialog, which) -> dialog.dismiss()) - .build() - .show(); + + dialog.show(); } } From 6087880a9ebc375061ab176cd6d02490b32284c1 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Wed, 27 Oct 2021 23:54:39 +0800 Subject: [PATCH 12/26] Add ShadowNativeOperations To fix builds with tests included. --- .../filesystem/root/ListFilesCommandTest.kt | 3 +- .../filesystem/root/ListFilesCommandTest2.kt | 3 +- .../test/ShadowNativeOperations.java | 37 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 app/src/test/java/com/amaze/filemanager/test/ShadowNativeOperations.java diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt index cca30ca2f1..6bce67c75c 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt @@ -30,6 +30,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.file_operations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.ShadowNativeOperations import com.amaze.filemanager.test.TestUtils import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants import org.junit.After @@ -47,7 +48,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( - shadows = [ShadowMultiDex::class], + shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], sdk = [JELLY_BEAN, KITKAT, P] ) /** diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt index dac4aa83d4..da02b61ddb 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt @@ -31,6 +31,7 @@ import com.amaze.filemanager.exceptions.ShellCommandInvalidException import com.amaze.filemanager.file_operations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.ShadowNativeOperations import com.amaze.filemanager.test.TestUtils import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants import org.junit.After @@ -48,7 +49,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( - shadows = [ShadowMultiDex::class], + shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], sdk = [JELLY_BEAN, KITKAT, P] ) /** diff --git a/app/src/test/java/com/amaze/filemanager/test/ShadowNativeOperations.java b/app/src/test/java/com/amaze/filemanager/test/ShadowNativeOperations.java new file mode 100644 index 0000000000..ebec6c41dc --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/test/ShadowNativeOperations.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014-2021 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.test; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +import com.amaze.filemanager.file_operations.filesystem.root.NativeOperations; + +import androidx.annotation.Nullable; + +@Implements(NativeOperations.class) +public class ShadowNativeOperations { + + @Implementation + public static boolean isDirectory(@Nullable String path) { + return path != null && path.startsWith("d"); + } +} From 1dd64e94b9c6f0128d5144f9c59943e30465f1ea Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Wed, 27 Oct 2021 00:09:36 +0800 Subject: [PATCH 13/26] Suppress StringLiteralDuplication for tests --- app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.kt b/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.kt index 6f0fc5148c..d9018b9363 100644 --- a/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ssh/SshClientUtilTest.kt @@ -24,6 +24,7 @@ import com.amaze.filemanager.filesystem.ssh.SshClientUtils import org.junit.Assert import org.junit.Test +@Suppress("StringLiteralDuplication") class SshClientUtilTest { /** * Test [SshClientUtils.extractRemotePathFrom]. From 10fb1eef8f030f0c591bcfd4fca5473edb42bd5b Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Tue, 26 Oct 2021 23:35:23 +0800 Subject: [PATCH 14/26] Create ShadowNativeOperations to make builds pass without native code --- .../filesystem/root/ListFilesCommandTest.kt | 3 +- .../filesystem/root/ListFilesCommandTest2.kt | 3 +- .../test/ShadowNativeOperations.java | 37 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 app/src/test/java/com/amaze/filemanager/test/ShadowNativeOperations.java diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt index cca30ca2f1..6bce67c75c 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt @@ -30,6 +30,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.file_operations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.ShadowNativeOperations import com.amaze.filemanager.test.TestUtils import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants import org.junit.After @@ -47,7 +48,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( - shadows = [ShadowMultiDex::class], + shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], sdk = [JELLY_BEAN, KITKAT, P] ) /** diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt index dac4aa83d4..da02b61ddb 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt @@ -31,6 +31,7 @@ import com.amaze.filemanager.exceptions.ShellCommandInvalidException import com.amaze.filemanager.file_operations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.ShadowNativeOperations import com.amaze.filemanager.test.TestUtils import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants import org.junit.After @@ -48,7 +49,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( - shadows = [ShadowMultiDex::class], + shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], sdk = [JELLY_BEAN, KITKAT, P] ) /** diff --git a/app/src/test/java/com/amaze/filemanager/test/ShadowNativeOperations.java b/app/src/test/java/com/amaze/filemanager/test/ShadowNativeOperations.java new file mode 100644 index 0000000000..ebec6c41dc --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/test/ShadowNativeOperations.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014-2021 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.test; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +import com.amaze.filemanager.file_operations.filesystem.root.NativeOperations; + +import androidx.annotation.Nullable; + +@Implements(NativeOperations.class) +public class ShadowNativeOperations { + + @Implementation + public static boolean isDirectory(@Nullable String path) { + return path != null && path.startsWith("d"); + } +} From 0e33ab919562c57a8beda75f4370add45af7ff4e Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Sun, 31 Oct 2021 03:49:56 +0530 Subject: [PATCH 15/26] 2910 Persist selected items when device is rotated --- .../filemanager/adapters/RecyclerAdapter.java | 54 +- .../ui/activities/MainActivity.java | 31 ++ .../ui/drag/RecyclerAdapterDragListener.kt | 10 +- .../ui/fragments/MainFragment.java | 399 +-------------- .../filemanager/ui/fragments/TabFragment.java | 6 +- .../fragments/data/MainFragmentViewModel.kt | 1 - .../filemanager/utils/ActionModeHelper.kt | 470 ++++++++++++++++++ .../amaze/filemanager/utils/DataUtils.java | 1 + .../com/amaze/filemanager/utils/Utils.java | 37 ++ 9 files changed, 601 insertions(+), 408 deletions(-) create mode 100644 app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt 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 cd94a3d676..dc9ad954e1 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -88,6 +88,7 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.view.ActionMode; import androidx.appcompat.view.ContextThemeWrapper; import androidx.recyclerview.widget.RecyclerView; @@ -199,6 +200,7 @@ public void toggleChecked(int position, ImageView imageView) { } if (!stoppedAnimation) mainFrag.stopAnimation(); + if (getItemsDigested().get(position).getChecked() == ListItem.CHECKED) { // if the view at position is checked, un-check it Log.d( @@ -227,34 +229,39 @@ public void toggleChecked(int position, ImageView imageView) { } else { // TODO: we don't have the check icon object probably because of config change } - if (mainFrag.mActionMode == null - || (mainFrag.getMainFragmentViewModel() != null - && !mainFrag.getMainFragmentViewModel().getSelection())) { - // start actionmode if not already started - // null condition if there is config change - if (mainFrag.getMainFragmentViewModel() != null) { - mainFrag.getMainFragmentViewModel().setSelection(true); - } - } } + invalidateSelection(); notifyItemChanged(position); } + private void invalidateSelection() { + if (mainFrag.getMainFragmentViewModel() != null) { + mainFrag + .getMainActivity() + .setListItemSelected(mainFrag.getMainFragmentViewModel().getCheckedItems().size() != 0); + } + } + public void invalidateActionMode() { if (mainFrag.getMainFragmentViewModel() != null) { // we have the actionmode visible, invalidate it's views - if (mainFrag.getMainFragmentViewModel().getSelection() && getCheckedItems().size() != 0) { - if (mainFrag.mActionMode == null) { - mainFrag.mActionMode = - mainFrag.getMainActivity().startSupportActionMode(mainFrag.mActionModeCallback); + if (mainFrag.getMainActivity().getListItemSelected()) { + if (mainFrag.getMainActivity().getActionModeHelper().getActionMode() == null) { + ActionMode.Callback mActionModeCallback = + mainFrag.getMainActivity().getActionModeHelper().getMActionModeCallback(); + mainFrag + .getMainActivity() + .getActionModeHelper() + .setActionMode( + mainFrag.getMainActivity().startSupportActionMode(mActionModeCallback)); } else { - mainFrag.mActionMode.invalidate(); + mainFrag.getMainActivity().getActionModeHelper().getActionMode().invalidate(); } } else { - if (mainFrag.mActionMode != null) { - mainFrag.mActionMode.finish(); - mainFrag.mActionMode = null; + if (mainFrag.getMainActivity().getActionModeHelper().getActionMode() != null) { + mainFrag.getMainActivity().getActionModeHelper().getActionMode().finish(); + mainFrag.getMainActivity().getActionModeHelper().setActionMode(null); } } } @@ -262,7 +269,6 @@ public void invalidateActionMode() { public void toggleChecked(boolean selectAll, String path) { int i = path.equals("/") || !getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON) ? 0 : 1; - for (; i < getItemsDigested().size(); i++) { ListItem item = getItemsDigested().get(i); if (selectAll && item.getChecked() != ListItem.CHECKED) { @@ -273,6 +279,8 @@ public void toggleChecked(boolean selectAll, String path) { notifyItemChanged(i); } } + invalidateSelection(); + invalidateActionMode(); } public void toggleInverse(String path) { @@ -396,6 +404,8 @@ public void toggleChecked(boolean b) { notifyItemChanged(i); } } + invalidateSelection(); + invalidateActionMode(); } public ArrayList getCheckedItems() { @@ -501,11 +511,11 @@ private void setItems( preloader = null; } - if (getItemsDigested() != null - && mainFrag.getMainFragmentViewModel().getIconList() != null - && invalidate) { + if (getItemsDigested() != null && invalidate) { getItemsDigested().clear(); - mainFrag.getMainFragmentViewModel().getIconList().clear(); + if (mainFrag.getMainFragmentViewModel().getIconList() != null) { + mainFrag.getMainFragmentViewModel().getIconList().clear(); + } } offset = 0; 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 4c1196b72a..f08a2453d3 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 @@ -118,6 +118,7 @@ import com.amaze.filemanager.ui.views.CustomZoomFocusChange; import com.amaze.filemanager.ui.views.appbar.AppBar; import com.amaze.filemanager.ui.views.drawer.Drawer; +import com.amaze.filemanager.utils.ActionModeHelper; import com.amaze.filemanager.utils.AppConstants; import com.amaze.filemanager.utils.BookSorter; import com.amaze.filemanager.utils.DataUtils; @@ -237,6 +238,7 @@ public class MainActivity extends PermissionsActivity private static final String KEY_OPERATED_ON_PATH = "oppathe1"; private static final String KEY_OPERATIONS_PATH_LIST = "oparraylist"; private static final String KEY_OPERATION = "operation"; + private static final String KEY_SELECTED_LIST_ITEM = "select_list_item"; private AppBar appbar; private Drawer drawer; @@ -294,6 +296,7 @@ public class MainActivity extends PermissionsActivity // the current visible tab, either 0 or 1 public static int currentTab; + private boolean listItemSelected = false; public static Shell.Interactive shellInteractive; public static Handler handler; @@ -304,6 +307,7 @@ public class MainActivity extends PermissionsActivity public static final int REQUEST_CODE_CLOUD_LIST_KEY = 5472; private PasteHelper pasteHelper; + private ActionModeHelper actionModeHelper; private static final String DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0"; private static final String INTERNAL_SHARED_STORAGE = "Internal shared storage"; @@ -323,6 +327,9 @@ public void onCreate(final Bundle savedInstanceState) { intent = getIntent(); dataUtils = DataUtils.getInstance(); + if (savedInstanceState != null) { + listItemSelected = savedInstanceState.getBoolean(KEY_SELECTED_LIST_ITEM, false); + } initialisePreferences(); initializeInteractiveShell(); @@ -337,6 +344,7 @@ public void onCreate(final Bundle savedInstanceState) { initialiseFab(); // TODO: 7/12/2017 not init when actionIntent != null mainActivityHelper = new MainActivityHelper(this); + actionModeHelper = new ActionModeHelper(MainActivity.this); if (CloudSheetFragment.isCloudProviderAvailable(this)) { @@ -1231,6 +1239,7 @@ public void onConfigurationChanged(Configuration newConfig) { protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(KEY_DRAWER_SELECTED, getDrawer().getDrawerSelectedItem()); + outState.putBoolean(KEY_SELECTED_LIST_ITEM, listItemSelected); if (pasteHelper != null) { outState.putParcelable(PASTEHELPER_BUNDLE, pasteHelper); } @@ -1812,6 +1821,10 @@ public PasteHelper getPaste() { return pasteHelper; } + public ActionModeHelper getActionModeHelper() { + return this.actionModeHelper; + } + public void setPaste(PasteHelper p) { pasteHelper = p; } @@ -2317,6 +2330,24 @@ public void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File } } + /** + * Get whether list item is selected for action mode or not + * + * @return value + */ + public boolean getListItemSelected() { + return this.listItemSelected; + } + + /** + * Set list item selected value + * + * @param value value + */ + public void setListItemSelected(boolean value) { + this.listItemSelected = value; + } + /** * Do nothing other than dismissing the folder selection dialog. * diff --git a/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt b/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt index 6716abc331..0eb325036b 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt @@ -138,7 +138,7 @@ class RecyclerAdapterDragListener( } DragEvent.ACTION_DROP -> { if (dragAndDropPref != PreferencesConstants.PREFERENCE_DRAG_TO_SELECT) { - var checkedItems: ArrayList = adapter.checkedItems + var checkedItems: ArrayList? = adapter.checkedItems var currentFileParcelable: HybridFileParcelable? = null var isCurrentElementDirectory: Boolean? = null var isEmptyArea: Boolean? = null @@ -156,7 +156,7 @@ class RecyclerAdapterDragListener( // dropping in goback button // hack to get the parent path val hybridFileParcelable = mainFragment - .elementsList!!.get(1).generateBaseFile() + .elementsList!![1].generateBaseFile() val hybridFile = HybridFile( hybridFileParcelable.mode, hybridFileParcelable.getParent(mainFragment.context) @@ -171,7 +171,7 @@ class RecyclerAdapterDragListener( } } } - if (checkedItems.size == 0) { + if (checkedItems?.size == 0) { // probably because we switched tabs and // this adapter doesn't have any checked items, get from data utils val dataUtils = DataUtils.getInstance() @@ -183,8 +183,8 @@ class RecyclerAdapterDragListener( ) checkedItems = dataUtils.checkedItemsList } - val arrayList = ArrayList(checkedItems.size) - checkedItems.forEach { + val arrayList = ArrayList() + checkedItems?.forEach { val file = it.generateBaseFile() if (it.desc.equals(pasteLocation) || ( diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java index 51c19e4d69..5295ecb153 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java @@ -54,7 +54,6 @@ import com.amaze.filemanager.filesystem.FileProperties; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; -import com.amaze.filemanager.filesystem.PasteHelper; import com.amaze.filemanager.filesystem.SafRootHolder; import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.CryptUtil; @@ -69,7 +68,6 @@ import com.amaze.filemanager.ui.fragments.data.MainFragmentViewModel; import com.amaze.filemanager.ui.icons.MimeTypes; import com.amaze.filemanager.ui.provider.UtilitiesProvider; -import com.amaze.filemanager.ui.selection.SelectionPopupMenu; import com.amaze.filemanager.ui.theme.AppTheme; import com.amaze.filemanager.ui.views.CustomScrollGridLayoutManager; import com.amaze.filemanager.ui.views.CustomScrollLinearLayoutManager; @@ -95,11 +93,9 @@ import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.media.RingtoneManager; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.DocumentsContract; @@ -108,9 +104,6 @@ import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; @@ -124,11 +117,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import androidx.appcompat.view.ActionMode; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; -import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; @@ -148,8 +139,6 @@ public class MainFragment extends Fragment ViewTreeObserver.OnGlobalLayoutListener, AdjustListViewForTv { - public ActionMode mActionMode; - public SwipeRefreshLayout mSwipeRefreshLayout; public RecyclerAdapter adapter; @@ -167,7 +156,6 @@ public class MainFragment extends Fragment private UtilitiesProvider utilsProvider; private HashMap scrolls = new HashMap<>(); private View rootView; - private View actionModeView; private FastScroller fastScroller; private CustomFileObserver customFileObserver; @@ -344,6 +332,8 @@ void switchToGrid() { setGridLayoutSpanSizeLookup(mLayoutManagerGrid); listView.setLayoutManager(mLayoutManagerGrid); listView.clearOnScrollListeners(); + mainFragmentViewModel.setAdapterListItems(null); + mainFragmentViewModel.setIconList(null); adapter = null; } @@ -358,6 +348,8 @@ void switchToList() { if (mLayoutManager == null) mLayoutManager = new CustomScrollLinearLayoutManager(getActivity()); listView.setLayoutManager(mLayoutManager); listView.clearOnScrollListeners(); + mainFragmentViewModel.setAdapterListItems(null); + mainFragmentViewModel.setIconList(null); adapter = null; } @@ -384,353 +376,6 @@ private void loadViews() { } } - public ActionMode.Callback mActionModeCallback = - new ActionMode.Callback() { - private void hideOption(int id, Menu menu) { - MenuItem item = menu.findItem(id); - item.setVisible(false); - } - - private void showOption(int id, Menu menu) { - MenuItem item = menu.findItem(id); - item.setVisible(true); - } - - void initMenu(Menu menu) { - /* - menu.findItem(R.id.cpy).setIcon(icons.getCopyDrawable()); - menu.findItem(R.id.cut).setIcon(icons.getCutDrawable()); - menu.findItem(R.id.delete).setIcon(icons.getDeleteDrawable()); - menu.findItem(R.id.all).setIcon(icons.getAllDrawable()); - */ - } - - // called when the action mode is created; startActionMode() was called - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - // Inflate a menu resource providing context menu items - MenuInflater inflater = mode.getMenuInflater(); - actionModeView = getActivity().getLayoutInflater().inflate(R.layout.actionmode, null); - mode.setCustomView(actionModeView); - - getMainActivity().setPagingEnabled(false); - getMainActivity().hideFab(); - - // translates the drawable content down - // if (getMainActivity().isDrawerLocked) getMainActivity().translateDrawerList(true); - - // assumes that you have "contexual.xml" menu resources - inflater.inflate(R.menu.contextual, menu); - initMenu(menu); - hideOption(R.id.addshortcut, menu); - hideOption(R.id.share, menu); - hideOption(R.id.openwith, menu); - if (getMainActivity().mReturnIntent) showOption(R.id.openmulti, menu); - // hideOption(R.id.setringtone,menu); - mode.setTitle(getResources().getString(R.string.select)); - - getMainActivity() - .updateViews(new ColorDrawable(res.getColor(R.color.holo_dark_action_mode))); - - // do not allow drawer to open when item gets selected - if (!getMainActivity().getDrawer().isLocked()) { - getMainActivity().getDrawer().lockIfNotOnTablet(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); - } - return true; - } - - /** - * the following method is called each time the action mode is shown. Always called after - * onCreateActionMode, but may be called multiple times if the mode is invalidated. - */ - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - ArrayList checkedItems = mainFragmentViewModel.getCheckedItems(); - actionModeView.setOnClickListener( - v -> - SelectionPopupMenu.Companion.invokeSelectionDropdown( - adapter, - actionModeView, - mainFragmentViewModel.getCurrentPath(), - getMainActivity())); - TextView textView = actionModeView.findViewById(R.id.item_count); - textView.setText(String.valueOf(checkedItems.size())); - hideOption(R.id.openmulti, menu); - menu.findItem(R.id.all) - .setTitle( - checkedItems.size() - == mainFragmentViewModel.getFolderCount() - + mainFragmentViewModel.getFileCount() - ? R.string.deselect_all - : R.string.select_all); - - if (mainFragmentViewModel.getOpenMode() != OpenMode.FILE) { - hideOption(R.id.addshortcut, menu); - hideOption(R.id.compress, menu); - return true; - } - - if (getMainActivity().mReturnIntent && SDK_INT >= JELLY_BEAN) { - showOption(R.id.openmulti, menu); - } - // tv.setText(checkedItems.size()); - if (!mainFragmentViewModel.getResults()) { - hideOption(R.id.openparent, menu); - if (checkedItems.size() == 1) { - showOption(R.id.addshortcut, menu); - showOption(R.id.openwith, menu); - showOption(R.id.share, menu); - - if (mainFragmentViewModel.getCheckedItems().get(0).isDirectory) { - hideOption(R.id.openwith, menu); - hideOption(R.id.share, menu); - hideOption(R.id.openmulti, menu); - } - - if (getMainActivity().mReturnIntent) - if (Build.VERSION.SDK_INT >= 16) showOption(R.id.openmulti, menu); - - } else { - try { - showOption(R.id.share, menu); - if (getMainActivity().mReturnIntent) - if (Build.VERSION.SDK_INT >= 16) showOption(R.id.openmulti, menu); - for (LayoutElementParcelable e : mainFragmentViewModel.getCheckedItems()) { - if (e.isDirectory) { - hideOption(R.id.share, menu); - hideOption(R.id.openmulti, menu); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - hideOption(R.id.openwith, menu); - hideOption(R.id.addshortcut, menu); - } - } else { - if (checkedItems.size() == 1) { - showOption(R.id.addshortcut, menu); - showOption(R.id.openparent, menu); - showOption(R.id.openwith, menu); - showOption(R.id.share, menu); - - if (mainFragmentViewModel.getCheckedItems().get(0).isDirectory) { - hideOption(R.id.openwith, menu); - hideOption(R.id.share, menu); - hideOption(R.id.openmulti, menu); - } - if (getMainActivity().mReturnIntent && SDK_INT >= JELLY_BEAN) { - showOption(R.id.openmulti, menu); - } - - } else { - hideOption(R.id.openparent, menu); - hideOption(R.id.addshortcut, menu); - - if (getMainActivity().mReturnIntent) - if (Build.VERSION.SDK_INT >= 16) showOption(R.id.openmulti, menu); - try { - for (LayoutElementParcelable e : mainFragmentViewModel.getCheckedItems()) { - if (e.isDirectory) { - hideOption(R.id.share, menu); - hideOption(R.id.openmulti, menu); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - - hideOption(R.id.openwith, menu); - } - } - - if (mainFragmentViewModel.getOpenMode() != OpenMode.FILE) { - hideOption(R.id.addshortcut, menu); - hideOption(R.id.compress, menu); - hideOption(R.id.hide, menu); - hideOption(R.id.addshortcut, menu); - } - return true; // Return false if nothing is done - } - - // called when the user selects a contextual menu item - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - computeScroll(); - ArrayList checkedItems = mainFragmentViewModel.getCheckedItems(); - switch (item.getItemId()) { - case R.id.openmulti: - try { - - Intent intent_result = new Intent(Intent.ACTION_SEND_MULTIPLE); - ArrayList resulturis = new ArrayList<>(); - - for (LayoutElementParcelable element : checkedItems) { - HybridFileParcelable baseFile = element.generateBaseFile(); - Uri resultUri = Utils.getUriForBaseFile(requireContext(), baseFile); - - if (resultUri != null) { - resulturis.add(resultUri); - } - } - - intent_result.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - requireActivity().setResult(FragmentActivity.RESULT_OK, intent_result); - intent_result.putParcelableArrayListExtra(Intent.EXTRA_STREAM, resulturis); - requireActivity().finish(); - // mode.finish(); - } catch (Exception e) { - e.printStackTrace(); - } - return true; - case R.id.about: - LayoutElementParcelable x = checkedItems.get(0); - GeneralDialogCreation.showPropertiesDialogWithPermissions( - x.generateBaseFile(), - x.permissions, - requireMainActivity(), - MainFragment.this, - requireMainActivity().isRootExplorer(), - utilsProvider.getAppTheme()); - mode.finish(); - return true; - case R.id.delete: - GeneralDialogCreation.deleteFilesDialog( - requireContext(), - requireMainActivity(), - checkedItems, - utilsProvider.getAppTheme()); - return true; - case R.id.share: - ArrayList arrayList = new ArrayList<>(); - for (LayoutElementParcelable e : checkedItems) { - arrayList.add(new File(e.desc)); - } - if (arrayList.size() > 100) - Toast.makeText( - getActivity(), - getResources().getString(R.string.share_limit), - Toast.LENGTH_SHORT) - .show(); - else { - - switch (mainFragmentViewModel.getListElements().get(0).getMode()) { - case DROPBOX: - case BOX: - case GDRIVE: - case ONEDRIVE: - FileUtils.shareCloudFile( - mainFragmentViewModel.getListElements().get(0).desc, - mainFragmentViewModel.getListElements().get(0).getMode(), - getContext()); - break; - default: - FileUtils.shareFiles( - arrayList, - getActivity(), - utilsProvider.getAppTheme(), - mainFragmentViewModel.getAccentColor()); - break; - } - } - return true; - case R.id.openparent: - loadlist(new File(checkedItems.get(0).desc).getParent(), false, OpenMode.FILE); - return true; - case R.id.all: - if (adapter.areAllChecked(mainFragmentViewModel.getCurrentPath())) { - adapter.toggleChecked(false, mainFragmentViewModel.getCurrentPath()); - item.setTitle(R.string.select_all); - } else { - adapter.toggleChecked(true, mainFragmentViewModel.getCurrentPath()); - item.setTitle(R.string.deselect_all); - } - mode.invalidate(); - - return true; - case R.id.rename: - final HybridFileParcelable f; - f = checkedItems.get(0).generateBaseFile(); - rename(f); - mode.finish(); - return true; - case R.id.hide: - for (int i1 = 0; i1 < checkedItems.size(); i1++) { - hide(checkedItems.get(i1).desc); - } - updateList(); - mode.finish(); - return true; - case R.id.ex: - getMainActivity().mainActivityHelper.extractFile(new File(checkedItems.get(0).desc)); - mode.finish(); - return true; - case R.id.cpy: - case R.id.cut: - { - HybridFileParcelable[] copies = new HybridFileParcelable[checkedItems.size()]; - for (int i = 0; i < checkedItems.size(); i++) { - copies[i] = checkedItems.get(i).generateBaseFile(); - } - int op = - item.getItemId() == R.id.cpy - ? PasteHelper.OPERATION_COPY - : PasteHelper.OPERATION_CUT; - // Making sure we don't cause an IllegalArgumentException - // when passing copies to PasteHelper - if (copies.length > 0) { - PasteHelper pasteHelper = new PasteHelper(getMainActivity(), op, copies); - requireMainActivity().setPaste(pasteHelper); - } - mode.finish(); - return true; - } - case R.id.compress: - ArrayList copies1 = new ArrayList<>(); - for (int i4 = 0; i4 < checkedItems.size(); i4++) { - copies1.add(checkedItems.get(i4).generateBaseFile()); - } - GeneralDialogCreation.showCompressDialog( - requireMainActivity(), copies1, mainFragmentViewModel.getCurrentPath()); - mode.finish(); - return true; - case R.id.openwith: - FileUtils.openFile( - new File(checkedItems.get(0).desc), requireMainActivity(), sharedPref); - return true; - case R.id.addshortcut: - addShortcut(checkedItems.get(0)); - mode.finish(); - return true; - default: - return false; - } - } - - // called when the user exits the action mode - public void onDestroyActionMode(ActionMode mode) { - mActionMode = null; - mainFragmentViewModel.setSelection(false); - - // translates the drawer content up - // if (getMainActivity().isDrawerLocked) getMainActivity().translateDrawerList(false); - - getMainActivity().showFab(); - if (!mainFragmentViewModel.getResults()) - adapter.toggleChecked(false, mainFragmentViewModel.getCurrentPath()); - else adapter.toggleChecked(false); - getMainActivity().setPagingEnabled(true); - - getMainActivity() - .updateViews( - new ColorDrawable( - MainActivity.currentTab == 1 - ? mainFragmentViewModel.getPrimaryTwoColor() - : mainFragmentViewModel.getPrimaryColor())); - - if (getMainActivity().getDrawer().isLocked()) { - getMainActivity().getDrawer().unlockIfNotOnTablet(); - } - } - }; - private BroadcastReceiver receiver2 = new BroadcastReceiver() { @@ -797,11 +442,13 @@ public void onListItemClicked( MainActivityHelper.SEARCH_TEXT = null; } - if (mainFragmentViewModel.getSelection()) { + if (getMainActivity().getListItemSelected()) { if (isBackButton) { - mainFragmentViewModel.setSelection(false); - if (mActionMode != null) mActionMode.finish(); - mActionMode = null; + getMainActivity().setListItemSelected(false); + if (getMainActivity().getActionModeHelper().getActionMode() != null) { + getMainActivity().getActionModeHelper().getActionMode().finish(); + } + getMainActivity().getActionModeHelper().setActionMode(null); } else { // the first {goback} item if back navigation is enabled adapter.toggleChecked(position, imageView); @@ -955,7 +602,11 @@ public void loadlist( return; } - if (mActionMode != null) mActionMode.finish(); + if (getMainActivity() != null + && getMainActivity().getActionModeHelper() != null + && getMainActivity().getActionModeHelper().getActionMode() != null) { + getMainActivity().getActionModeHelper().getActionMode().finish(); + } mSwipeRefreshLayout.setRefreshing(true); @@ -1366,7 +1017,7 @@ public void goBack() { if (!mainFragmentViewModel.getResults()) { if (!mainFragmentViewModel.getRetainSearchTask()) { // normal case - if (mainFragmentViewModel.getSelection()) { + if (getMainActivity().getListItemSelected()) { adapter.toggleChecked(false); } else { if (OpenMode.SMB.equals(mainFragmentViewModel.getOpenMode())) { @@ -1514,7 +1165,7 @@ public void goBackItemClick() { HybridFile currentFile = new HybridFile(mainFragmentViewModel.getOpenMode(), mainFragmentViewModel.getCurrentPath()); if (!mainFragmentViewModel.getResults()) { - if (mainFragmentViewModel.getSelection()) { + if (getMainActivity().getListItemSelected()) { adapter.toggleChecked(false); } else { if (mainFragmentViewModel.getOpenMode() == OpenMode.SMB) { @@ -1733,7 +1384,7 @@ public void hide(String path) { } } - private void addShortcut(LayoutElementParcelable path) { + public void addShortcut(LayoutElementParcelable path) { // Adding shortcut for MainActivity // on Home screen final Context ctx = getContext(); @@ -1939,14 +1590,6 @@ public void initTopAndEmptyAreaDragListeners(boolean destroy) { } } - public void disableActionMode() { - mainFragmentViewModel.setSelection(false); - if (this.mActionMode != null) { - this.mActionMode.finish(); - } - this.mActionMode = null; - } - public void smoothScrollListView(boolean upDirection) { if (listView != null) { if (upDirection) { @@ -2004,9 +1647,11 @@ public void onGlobalLayout() { mLayoutManagerGrid.setSpanCount(mainFragmentViewModel.getColumns()); } } - if (!mainFragmentViewModel.isList()) { + // TODO: This trigger causes to lose selected items in case of grid view, + // but is necessary to adjust columns for grid view when screen is rotated + /*if (!mainFragmentViewModel.isList()) { loadViews(); - } + }*/ if (android.os.Build.VERSION.SDK_INT >= JELLY_BEAN) { mToolbarContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); } else { diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java index b588a5b9be..f41a8514fb 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java @@ -253,7 +253,7 @@ public void onPageScrolled(int position, float positionOffset, int positionOffse final MainFragment mainFragment = mainActivity.getCurrentMainFragment(); if (mainFragment == null || mainFragment.getMainFragmentViewModel() == null - || mainFragment.getMainFragmentViewModel().getSelection()) { + || mainFragment.getMainActivity().getListItemSelected()) { return; // we do not want to update toolbar colors when ActionMode is activated } @@ -476,7 +476,7 @@ private void initLeftAndRightDragListeners(boolean destroy) { if (mViewPager.getCurrentItem() == 1) { if (mainFragment != null) { dataUtils.setCheckedItemsList(mainFragment.adapter.getCheckedItems()); - mainFragment.disableActionMode(); + mainActivity.getActionModeHelper().disableActionMode(); } mViewPager.setCurrentItem(0, true); } @@ -488,7 +488,7 @@ private void initLeftAndRightDragListeners(boolean destroy) { if (mViewPager.getCurrentItem() == 0) { if (mainFragment != null) { dataUtils.setCheckedItemsList(mainFragment.adapter.getCheckedItems()); - mainFragment.disableActionMode(); + mainActivity.getActionModeHelper().disableActionMode(); } mViewPager.setCurrentItem(1, true); } diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt index 9b82d4089b..ea7ed71c29 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt @@ -54,7 +54,6 @@ class MainFragmentViewModel : ViewModel() { var dsort = 0 var asc = 0 var home: String? = null - var selection = false var results: Boolean = false lateinit var openMode: OpenMode diff --git a/app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt b/app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt new file mode 100644 index 0000000000..ec27b8b714 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2014-2021 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.graphics.drawable.ColorDrawable +import android.net.Uri +import android.os.Build +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.view.ActionMode +import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.FragmentActivity +import com.amaze.filemanager.R +import com.amaze.filemanager.adapters.data.LayoutElementParcelable +import com.amaze.filemanager.file_operations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.HybridFileParcelable +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.selection.SelectionPopupMenu.Companion.invokeSelectionDropdown +import java.io.File +import java.lang.Exception +import java.util.ArrayList + +class ActionModeHelper(val mainActivity: MainActivity) { + + var actionModeView: View? = null + var actionMode: ActionMode? = null + + var mActionModeCallback: ActionMode.Callback = object : ActionMode.Callback { + private fun hideOption(id: Int, menu: Menu) { + val item = menu.findItem(id) + item.isVisible = false + } + + private fun showOption(id: Int, menu: Menu) { + val item = menu.findItem(id) + item.isVisible = true + } + + fun initMenu(menu: Menu?) { + /* + menu.findItem(R.id.cpy).setIcon(icons.getCopyDrawable()); + menu.findItem(R.id.cut).setIcon(icons.getCutDrawable()); + menu.findItem(R.id.delete).setIcon(icons.getDeleteDrawable()); + menu.findItem(R.id.all).setIcon(icons.getAllDrawable()); + */ + } + + // called when the action mode is created; startActionMode() was called + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + // Inflate a menu resource providing context menu items + val inflater = mode.menuInflater + actionModeView = mainActivity.layoutInflater.inflate(R.layout.actionmode, null) + mode.customView = actionModeView + mainActivity.setPagingEnabled(false) + mainActivity.hideFab() + + // translates the drawable content down + // if (mainActivity.isDrawerLocked) mainActivity.translateDrawerList(true); + + // assumes that you have "contexual.xml" menu resources + inflater.inflate(R.menu.contextual, menu) + initMenu(menu) + hideOption(R.id.addshortcut, menu) + hideOption(R.id.share, menu) + hideOption(R.id.openwith, menu) + if (mainActivity.mReturnIntent) showOption(R.id.openmulti, menu) + // hideOption(R.id.setringtone,menu); + mode.title = mainActivity.resources.getString(R.string.select) + mainActivity + .updateViews( + ColorDrawable( + mainActivity.resources + .getColor(R.color.holo_dark_action_mode) + ) + ) + + // do not allow drawer to open when item gets selected + if (!mainActivity.drawer.isLocked) { + mainActivity.drawer.lockIfNotOnTablet(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + } + return true + } + + /** + * the following method is called each time the action mode is shown. Always called after + * onCreateActionMode, but may be called multiple times if the mode is invalidated. + */ + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + safeLet( + mainActivity.currentMainFragment?.mainFragmentViewModel, + mainActivity.currentMainFragment?.adapter + ) { + mainFragmentViewModel, adapter -> + val checkedItems: ArrayList = + mainFragmentViewModel.getCheckedItems() + actionModeView?.setOnClickListener( + View.OnClickListener { v: View? -> + invokeSelectionDropdown( + adapter, + actionModeView!!, + mainFragmentViewModel.currentPath!!, + mainActivity + ) + } + ) + val textView: TextView = actionModeView!!.findViewById(R.id.item_count) + textView.text = checkedItems.size.toString() + hideOption(R.id.openmulti, menu) + menu.findItem(R.id.all) + .setTitle( + if (checkedItems.size + == mainFragmentViewModel.folderCount + + mainFragmentViewModel.fileCount + ) R.string.deselect_all else R.string.select_all + ) + if (mainFragmentViewModel.openMode != OpenMode.FILE) { + hideOption(R.id.addshortcut, menu) + hideOption(R.id.compress, menu) + return true + } + if (mainActivity.mReturnIntent && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + ) { + showOption(R.id.openmulti, menu) + } + // tv.setText(checkedItems.size()); + if (!mainFragmentViewModel.results) { + hideOption(R.id.openparent, menu) + if (checkedItems.size == 1) { + showOption(R.id.addshortcut, menu) + showOption(R.id.openwith, menu) + showOption(R.id.share, menu) + if (mainFragmentViewModel.getCheckedItems().get(0).isDirectory) { + hideOption(R.id.openwith, menu) + hideOption(R.id.share, menu) + hideOption(R.id.openmulti, menu) + } + if (mainActivity.mReturnIntent) { + if (Build.VERSION.SDK_INT >= 16) showOption( + R.id.openmulti, + menu + ) + } + } else { + try { + showOption(R.id.share, menu) + if (mainActivity.mReturnIntent && Build.VERSION.SDK_INT >= 16) { + showOption( + R.id.openmulti, + menu + ) + } + for (e in mainFragmentViewModel.getCheckedItems()) { + if (e.isDirectory) { + hideOption(R.id.share, menu) + hideOption(R.id.openmulti, menu) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + hideOption(R.id.openwith, menu) + hideOption(R.id.addshortcut, menu) + } + } else { + if (checkedItems.size == 1) { + showOption(R.id.addshortcut, menu) + showOption(R.id.openparent, menu) + showOption(R.id.openwith, menu) + showOption(R.id.share, menu) + if (mainFragmentViewModel.getCheckedItems()[0].isDirectory) { + hideOption(R.id.openwith, menu) + hideOption(R.id.share, menu) + hideOption(R.id.openmulti, menu) + } + if (mainActivity.mReturnIntent && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + ) { + showOption(R.id.openmulti, menu) + } + } else { + hideOption(R.id.openparent, menu) + hideOption(R.id.addshortcut, menu) + if (mainActivity.mReturnIntent && Build.VERSION.SDK_INT >= 16) { + showOption( + R.id.openmulti, + menu + ) + } + try { + for (e in mainFragmentViewModel.getCheckedItems()) { + if (e.isDirectory) { + hideOption(R.id.share, menu) + hideOption(R.id.openmulti, menu) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + hideOption(R.id.openwith, menu) + } + } + if (mainFragmentViewModel.openMode != OpenMode.FILE) { + hideOption(R.id.addshortcut, menu) + hideOption(R.id.compress, menu) + hideOption(R.id.hide, menu) + hideOption(R.id.addshortcut, menu) + } + } + return true // Return false if nothing is done + } + + // called when the user selects a contextual menu item + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + mainActivity.currentMainFragment?.computeScroll() + mainActivity.currentMainFragment?.mainFragmentViewModel?.getCheckedItems()?.also { + checkedItems -> + return when (item.itemId) { + R.id.openmulti -> { + try { + val intent_result = Intent(Intent.ACTION_SEND_MULTIPLE) + val resulturis = ArrayList() + for (element in checkedItems) { + val baseFile = element.generateBaseFile() + val resultUri = Utils.getUriForBaseFile(mainActivity, baseFile) + if (resultUri != null) { + resulturis.add(resultUri) + } + } + intent_result.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + mainActivity.setResult(FragmentActivity.RESULT_OK, intent_result) + intent_result.putParcelableArrayListExtra( + Intent.EXTRA_STREAM, + resulturis + ) + mainActivity.finish() + // mode.finish(); + } catch (e: Exception) { + e.printStackTrace() + } + true + } + R.id.about -> { + val x = checkedItems[0] + mainActivity.currentMainFragment?.also { + GeneralDialogCreation.showPropertiesDialogWithPermissions( + x.generateBaseFile(), + x.permissions, + mainActivity, + it, + mainActivity.isRootExplorer, + mainActivity.utilsProvider.appTheme + ) + } + mode.finish() + true + } + R.id.delete -> { + GeneralDialogCreation.deleteFilesDialog( + mainActivity, + mainActivity, + checkedItems, + mainActivity.utilsProvider.appTheme + ) + true + } + R.id.share -> { + val arrayList = ArrayList() + for (e in checkedItems) { + arrayList.add(File(e.desc)) + } + if (arrayList.size > 100) Toast.makeText( + mainActivity, + mainActivity.resources.getString(R.string.share_limit), + Toast.LENGTH_SHORT + ) + .show() else { + mainActivity.currentMainFragment?.mainFragmentViewModel?.also { + mainFragmentViewModel -> + when (mainFragmentViewModel.listElements?.get(0)?.mode) { + OpenMode.DROPBOX, OpenMode.BOX, OpenMode.GDRIVE, + OpenMode.ONEDRIVE -> + mainFragmentViewModel.listElements?.also { + FileUtils.shareCloudFile( + it[0].desc, + it[0].mode, + mainActivity + ) + } + else -> FileUtils.shareFiles( + arrayList, + mainActivity, + mainActivity.utilsProvider.appTheme, + mainFragmentViewModel.accentColor + ) + } + } + } + true + } + R.id.openparent -> { + mainActivity.currentMainFragment?.loadlist( + File(checkedItems[0].desc).parent, + false, OpenMode.FILE + ) + + true + } + R.id.all -> { + safeLet( + mainActivity.currentMainFragment?.mainFragmentViewModel, + mainActivity.currentMainFragment?.adapter + ) { + mainFragmentViewModel, adapter -> + if (adapter.areAllChecked(mainFragmentViewModel.currentPath)) { + adapter.toggleChecked( + false, + mainFragmentViewModel.currentPath + ) + item.setTitle(R.string.select_all) + } else { + adapter.toggleChecked( + true, + mainFragmentViewModel.currentPath + ) + item.setTitle(R.string.deselect_all) + } + } + mode.invalidate() + true + } + R.id.rename -> { + val f: HybridFileParcelable = checkedItems[0].generateBaseFile() + mainActivity.currentMainFragment?.rename(f) + mode.finish() + true + } + R.id.hide -> { + var i1 = 0 + while (i1 < checkedItems.size) { + mainActivity.currentMainFragment?.hide(checkedItems[i1].desc) + i1++ + } + mainActivity.currentMainFragment?.updateList() + mode.finish() + true + } + R.id.ex -> { + mainActivity.mainActivityHelper.extractFile(File(checkedItems[0].desc)) + mode.finish() + true + } + R.id.cpy, R.id.cut -> { + val copies = arrayOfNulls(checkedItems.size) + var i = 0 + while (i < checkedItems.size) { + copies[i] = checkedItems[i].generateBaseFile() + i++ + } + val op = + if (item.itemId == R.id.cpy) PasteHelper.OPERATION_COPY + else PasteHelper.OPERATION_CUT + // Making sure we don't cause an IllegalArgumentException + // when passing copies to PasteHelper + if (copies.isNotEmpty()) { + val pasteHelper = PasteHelper(mainActivity, op, copies) + mainActivity.paste = pasteHelper + } + mode.finish() + true + } + R.id.compress -> { + val copies1 = ArrayList() + var i4 = 0 + while (i4 < checkedItems.size) { + copies1.add(checkedItems[i4].generateBaseFile()) + i4++ + } + GeneralDialogCreation.showCompressDialog( + mainActivity, copies1, + mainActivity.currentMainFragment?.mainFragmentViewModel?.currentPath + ) + mode.finish() + true + } + R.id.openwith -> { + FileUtils.openFile( + File(checkedItems[0].desc), mainActivity, mainActivity.prefs + ) + true + } + R.id.addshortcut -> { + Utils.addShortcut( + mainActivity, mainActivity.componentName, + checkedItems[0] + ) + mode.finish() + true + } + else -> false + } + } + return false + } + + // called when the user exits the action mode + override fun onDestroyActionMode(mode: ActionMode) { + actionMode = null + mainActivity.listItemSelected = false + + // translates the drawer content up + // if (mainActivity.isDrawerLocked) mainActivity.translateDrawerList(false); + mainActivity.showFab() + + mainActivity.setPagingEnabled(true) + safeLet( + mainActivity.currentMainFragment?.mainFragmentViewModel, + mainActivity.currentMainFragment?.adapter + ) { + mainFragmentViewModel, adapter -> + if (!mainFragmentViewModel.results) + adapter.toggleChecked(false, mainFragmentViewModel.currentPath) + else adapter.toggleChecked(false) + mainActivity + .updateViews( + ColorDrawable( + if (MainActivity.currentTab == 1) + mainFragmentViewModel.primaryTwoColor + else mainFragmentViewModel.primaryColor + ) + ) + } + + if (mainActivity.drawer.isLocked) { + mainActivity.drawer.unlockIfNotOnTablet() + } + } + } + + fun disableActionMode() { + mainActivity.listItemSelected = false + actionMode?.finish() + actionMode = null + } +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java b/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java index a4a79ce6e6..42a0149b1d 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java +++ b/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java @@ -71,6 +71,7 @@ public class DataUtils { private ArrayList accounts = new ArrayList<>(4); + /** List of checked items to persist when drag and drop from one tab to another */ private ArrayList checkedItemsList; private DataChangeListener dataChangeListener; diff --git a/app/src/main/java/com/amaze/filemanager/utils/Utils.java b/app/src/main/java/com/amaze/filemanager/utils/Utils.java index 88c7eab5f4..1e033516be 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/Utils.java +++ b/app/src/main/java/com/amaze/filemanager/utils/Utils.java @@ -27,6 +27,7 @@ import com.amaze.filemanager.BuildConfig; import com.amaze.filemanager.R; +import com.amaze.filemanager.adapters.data.LayoutElementParcelable; import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.theme.AppTheme; @@ -34,6 +35,7 @@ import android.annotation.TargetApi; import android.app.Activity; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -59,7 +61,10 @@ import androidx.cardview.widget.CardView; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.graphics.drawable.IconCompat; /** * Contains useful functions and methods (NOTHING HERE DEALS WITH FILES) @@ -407,4 +412,36 @@ public static void zoom(Float scaleX, Float scaleY, PointF pivot, View view) { view.setScaleX(scaleX); view.setScaleY(scaleY); } + + public static void addShortcut( + Context context, ComponentName componentName, LayoutElementParcelable path) { + // Adding shortcut for MainActivity + // on Home screen + + if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { + Toast.makeText( + context, + context.getString(R.string.add_shortcut_not_supported_by_launcher), + Toast.LENGTH_SHORT) + .show(); + return; + } + + Intent shortcutIntent = new Intent(context, MainActivity.class); + shortcutIntent.putExtra("path", path.desc); + shortcutIntent.setAction(Intent.ACTION_MAIN); + shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + + // Using file path as shortcut id. + ShortcutInfoCompat info = + new ShortcutInfoCompat.Builder(context, path.desc) + .setActivity(componentName) + .setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher)) + .setIntent(shortcutIntent) + .setLongLabel(path.desc) + .setShortLabel(new File(path.desc).getName()) + .build(); + + ShortcutManagerCompat.requestPinShortcut(context, info, null); + } } From 80d012bb8986f4874944846b7665684c187cd961 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Sun, 31 Oct 2021 05:18:34 +0530 Subject: [PATCH 16/26] 2910 Fix codacy issues --- .../filemanager/adapters/RecyclerAdapter.java | 9 +- .../fragments/data/MainFragmentViewModel.kt | 3 + .../filemanager/utils/ActionModeHelper.kt | 96 ++++++++----------- 3 files changed, 49 insertions(+), 59 deletions(-) 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 dc9ad954e1..25fe3dd929 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -533,11 +533,10 @@ private void setItems( if (mainFrag.getMainFragmentViewModel() != null && mainFrag.getMainFragmentViewModel().isList() - && listItems.size() > 0) { - if (getItemsDigested() == null || invalidate) { - listItems.add(new ListItem(EMPTY_LAST_ITEM)); - uris.add(null); - } + && listItems.size() > 0 + && (getItemsDigested() == null || invalidate)) { + listItems.add(new ListItem(EMPTY_LAST_ITEM)); + uris.add(null); } if (getItemsDigested() == null || invalidate) { diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt index ea7ed71c29..c6a70ade17 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt @@ -196,6 +196,9 @@ class MainFragmentViewModel : ViewModel() { CloudHandler.CLOUD_PREFIX_DROPBOX + "/" == currentPath } + /** + * Get checked items in adapter + */ fun getCheckedItems(): ArrayList { val selected = ArrayList() for (i in adapterListItems!!.indices) { diff --git a/app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt b/app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt index ec27b8b714..88c636afae 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt @@ -42,7 +42,6 @@ import com.amaze.filemanager.ui.activities.MainActivity import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation import com.amaze.filemanager.ui.selection.SelectionPopupMenu.Companion.invokeSelectionDropdown import java.io.File -import java.lang.Exception import java.util.ArrayList class ActionModeHelper(val mainActivity: MainActivity) { @@ -118,17 +117,15 @@ class ActionModeHelper(val mainActivity: MainActivity) { mainFragmentViewModel, adapter -> val checkedItems: ArrayList = mainFragmentViewModel.getCheckedItems() - actionModeView?.setOnClickListener( - View.OnClickListener { v: View? -> - invokeSelectionDropdown( - adapter, - actionModeView!!, - mainFragmentViewModel.currentPath!!, - mainActivity - ) - } - ) - val textView: TextView = actionModeView!!.findViewById(R.id.item_count) + actionModeView?.setOnClickListener { + invokeSelectionDropdown( + adapter, + actionModeView!!, + mainFragmentViewModel.currentPath!!, + mainActivity + ) + } + val textView: TextView = actionModeView!!.findViewById(R.id.item_count) textView.text = checkedItems.size.toString() hideOption(R.id.openmulti, menu) menu.findItem(R.id.all) @@ -167,22 +164,18 @@ class ActionModeHelper(val mainActivity: MainActivity) { ) } } else { - try { - showOption(R.id.share, menu) - if (mainActivity.mReturnIntent && Build.VERSION.SDK_INT >= 16) { - showOption( - R.id.openmulti, - menu - ) - } - for (e in mainFragmentViewModel.getCheckedItems()) { - if (e.isDirectory) { - hideOption(R.id.share, menu) - hideOption(R.id.openmulti, menu) - } + showOption(R.id.share, menu) + if (mainActivity.mReturnIntent && Build.VERSION.SDK_INT >= 16) { + showOption( + R.id.openmulti, + menu + ) + } + for (e in mainFragmentViewModel.getCheckedItems()) { + if (e.isDirectory) { + hideOption(R.id.share, menu) + hideOption(R.id.openmulti, menu) } - } catch (e: Exception) { - e.printStackTrace() } hideOption(R.id.openwith, menu) hideOption(R.id.addshortcut, menu) @@ -212,15 +205,11 @@ class ActionModeHelper(val mainActivity: MainActivity) { menu ) } - try { - for (e in mainFragmentViewModel.getCheckedItems()) { - if (e.isDirectory) { - hideOption(R.id.share, menu) - hideOption(R.id.openmulti, menu) - } + for (e in mainFragmentViewModel.getCheckedItems()) { + if (e.isDirectory) { + hideOption(R.id.share, menu) + hideOption(R.id.openmulti, menu) } - } catch (e: Exception) { - e.printStackTrace() } hideOption(R.id.openwith, menu) } @@ -242,27 +231,23 @@ class ActionModeHelper(val mainActivity: MainActivity) { checkedItems -> return when (item.itemId) { R.id.openmulti -> { - try { - val intent_result = Intent(Intent.ACTION_SEND_MULTIPLE) - val resulturis = ArrayList() - for (element in checkedItems) { - val baseFile = element.generateBaseFile() - val resultUri = Utils.getUriForBaseFile(mainActivity, baseFile) - if (resultUri != null) { - resulturis.add(resultUri) - } + val intent_result = Intent(Intent.ACTION_SEND_MULTIPLE) + val resulturis = ArrayList() + for (element in checkedItems) { + val baseFile = element.generateBaseFile() + val resultUri = Utils.getUriForBaseFile(mainActivity, baseFile) + if (resultUri != null) { + resulturis.add(resultUri) } - intent_result.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - mainActivity.setResult(FragmentActivity.RESULT_OK, intent_result) - intent_result.putParcelableArrayListExtra( - Intent.EXTRA_STREAM, - resulturis - ) - mainActivity.finish() - // mode.finish(); - } catch (e: Exception) { - e.printStackTrace() } + intent_result.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + mainActivity.setResult(FragmentActivity.RESULT_OK, intent_result) + intent_result.putParcelableArrayListExtra( + Intent.EXTRA_STREAM, + resulturis + ) + mainActivity.finish() + // mode.finish(); true } R.id.about -> { @@ -462,6 +447,9 @@ class ActionModeHelper(val mainActivity: MainActivity) { } } + /** + * Finishes the action mode + */ fun disableActionMode() { mainActivity.listItemSelected = false actionMode?.finish() From 8f1ee07d8a9ae5af35e565350574c74e0b03d9a7 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Tue, 2 Nov 2021 01:41:50 +0530 Subject: [PATCH 17/26] 2910 Fix comments --- .../amaze/filemanager/adapters/RecyclerAdapter.java | 5 ++++- .../filemanager/ui/activities/MainActivity.java | 10 +++++----- .../ui/fragments/data/MainFragmentViewModel.kt | 6 +++--- ...ActionModeHelper.kt => MainActivityActionMode.kt} | 12 +----------- 4 files changed, 13 insertions(+), 20 deletions(-) rename app/src/main/java/com/amaze/filemanager/utils/{ActionModeHelper.kt => MainActivityActionMode.kt} (97%) 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 25fe3dd929..540076e7c8 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -412,8 +412,11 @@ public ArrayList getCheckedItems() { return mainFrag.getMainFragmentViewModel().getCheckedItems(); } + @Nullable public ArrayList getItemsDigested() { - return mainFrag.getMainFragmentViewModel().getAdapterListItems(); + return mainFrag.getMainFragmentViewModel() != null + ? mainFrag.getMainFragmentViewModel().getAdapterListItems() + : null; } public boolean areAllChecked(String path) { 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 0d25bdc4f7..7f3d35b235 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 @@ -118,10 +118,10 @@ import com.amaze.filemanager.ui.views.CustomZoomFocusChange; import com.amaze.filemanager.ui.views.appbar.AppBar; import com.amaze.filemanager.ui.views.drawer.Drawer; -import com.amaze.filemanager.utils.ActionModeHelper; import com.amaze.filemanager.utils.AppConstants; import com.amaze.filemanager.utils.BookSorter; import com.amaze.filemanager.utils.DataUtils; +import com.amaze.filemanager.utils.MainActivityActionMode; import com.amaze.filemanager.utils.MainActivityHelper; import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.PreferenceUtils; @@ -307,7 +307,7 @@ public class MainActivity extends PermissionsActivity public static final int REQUEST_CODE_CLOUD_LIST_KEY = 5472; private PasteHelper pasteHelper; - private ActionModeHelper actionModeHelper; + private MainActivityActionMode mainActivityActionMode; private static final String DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0"; private static final String INTERNAL_SHARED_STORAGE = "Internal shared storage"; @@ -344,7 +344,7 @@ public void onCreate(final Bundle savedInstanceState) { initialiseFab(); // TODO: 7/12/2017 not init when actionIntent != null mainActivityHelper = new MainActivityHelper(this); - actionModeHelper = new ActionModeHelper(MainActivity.this); + mainActivityActionMode = new MainActivityActionMode(MainActivity.this); if (CloudSheetFragment.isCloudProviderAvailable(this)) { @@ -1821,8 +1821,8 @@ public PasteHelper getPaste() { return pasteHelper; } - public ActionModeHelper getActionModeHelper() { - return this.actionModeHelper; + public MainActivityActionMode getActionModeHelper() { + return this.mainActivityActionMode; } public void setPaste(PasteHelper p) { diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt index c6a70ade17..f95d8b6fae 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt @@ -201,9 +201,9 @@ class MainFragmentViewModel : ViewModel() { */ fun getCheckedItems(): ArrayList { val selected = ArrayList() - for (i in adapterListItems!!.indices) { - if (adapterListItems!![i].checked == RecyclerAdapter.ListItem.CHECKED) { - selected.add(adapterListItems!![i].elem) + adapterListItems?.forEach { item -> + if (item.checked == RecyclerAdapter.ListItem.CHECKED) { + selected.add(item.elem) } } return selected diff --git a/app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt b/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt similarity index 97% rename from app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt rename to app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt index 88c636afae..21cd278347 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/ActionModeHelper.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt @@ -44,7 +44,7 @@ import com.amaze.filemanager.ui.selection.SelectionPopupMenu.Companion.invokeSel import java.io.File import java.util.ArrayList -class ActionModeHelper(val mainActivity: MainActivity) { +class MainActivityActionMode(val mainActivity: MainActivity) { var actionModeView: View? = null var actionMode: ActionMode? = null @@ -60,15 +60,6 @@ class ActionModeHelper(val mainActivity: MainActivity) { item.isVisible = true } - fun initMenu(menu: Menu?) { - /* - menu.findItem(R.id.cpy).setIcon(icons.getCopyDrawable()); - menu.findItem(R.id.cut).setIcon(icons.getCutDrawable()); - menu.findItem(R.id.delete).setIcon(icons.getDeleteDrawable()); - menu.findItem(R.id.all).setIcon(icons.getAllDrawable()); - */ - } - // called when the action mode is created; startActionMode() was called override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { // Inflate a menu resource providing context menu items @@ -83,7 +74,6 @@ class ActionModeHelper(val mainActivity: MainActivity) { // assumes that you have "contexual.xml" menu resources inflater.inflate(R.menu.contextual, menu) - initMenu(menu) hideOption(R.id.addshortcut, menu) hideOption(R.id.share, menu) hideOption(R.id.openwith, menu) From 6eb0ba77edcc75476b7cd6ff6e14221657d43380 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Tue, 2 Nov 2021 03:36:57 +0530 Subject: [PATCH 18/26] 2910 Fix comments --- .../ui/drag/RecyclerAdapterDragListener.kt | 59 ++++++++++--------- .../ui/selection/SelectionPopupMenu.kt | 7 ++- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt b/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt index 0eb325036b..4cd8d27fba 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt @@ -33,6 +33,7 @@ import com.amaze.filemanager.ui.dialogs.DragAndDropDialog import com.amaze.filemanager.ui.fragments.MainFragment import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants import com.amaze.filemanager.utils.DataUtils +import com.amaze.filemanager.utils.safeLet import kotlin.collections.ArrayList class RecyclerAdapterDragListener( @@ -64,11 +65,12 @@ class RecyclerAdapterDragListener( true } DragEvent.ACTION_DRAG_ENTERED -> { - holder?.run { - if (adapter.itemsDigested.size != 0 && - holder.adapterPosition < adapter.itemsDigested.size + safeLet(holder, adapter.itemsDigested) { + holder, itemsDigested -> + if (itemsDigested.size != 0 && + holder.adapterPosition < itemsDigested.size ) { - val listItem = (adapter.itemsDigested[holder.adapterPosition]) + val listItem = (itemsDigested[holder.adapterPosition]) if (dragAndDropPref == PreferencesConstants.PREFERENCE_DRAG_TO_SELECT) { if (listItem.specialType != RecyclerAdapter.TYPE_BACK && listItem.shouldToggleDragChecked @@ -96,11 +98,12 @@ class RecyclerAdapterDragListener( true } DragEvent.ACTION_DRAG_EXITED -> { - holder?.run { - if (adapter.itemsDigested.size != 0 && - holder.adapterPosition < adapter.itemsDigested.size + safeLet(holder, adapter.itemsDigested) { + holder, itemsDigested -> + if (itemsDigested.size != 0 && + holder.adapterPosition < itemsDigested.size ) { - val listItem = (adapter.itemsDigested[holder.adapterPosition]) + val listItem = (itemsDigested[holder.adapterPosition]) if (dragAndDropPref != PreferencesConstants.PREFERENCE_DRAG_TO_SELECT) { val checkedItems: ArrayList = adapter.checkedItems @@ -142,7 +145,7 @@ class RecyclerAdapterDragListener( var currentFileParcelable: HybridFileParcelable? = null var isCurrentElementDirectory: Boolean? = null var isEmptyArea: Boolean? = null - var pasteLocation: String? = if (adapter.itemsDigested.size == 0) { + var pasteLocation: String? = if (adapter.itemsDigested?.size == 0) { mainFragment.currentPath } else { if (holder == null || holder.adapterPosition == RecyclerView.NO_POSITION) { @@ -150,24 +153,26 @@ class RecyclerAdapterDragListener( isEmptyArea = true mainFragment.currentPath } else { - if (adapter.itemsDigested[holder.adapterPosition].specialType - == RecyclerAdapter.TYPE_BACK - ) { - // dropping in goback button - // hack to get the parent path - val hybridFileParcelable = mainFragment - .elementsList!![1].generateBaseFile() - val hybridFile = HybridFile( - hybridFileParcelable.mode, - hybridFileParcelable.getParent(mainFragment.context) - ) - hybridFile.getParent(mainFragment.context) - } else { - val currentElement = adapter - .itemsDigested[holder.adapterPosition].elem - currentFileParcelable = currentElement.generateBaseFile() - isCurrentElementDirectory = currentElement.isDirectory - currentElement.desc + adapter.itemsDigested?.let { + itemsDigested -> + if (itemsDigested[holder.adapterPosition].specialType + == RecyclerAdapter.TYPE_BACK + ) { + // dropping in goback button + // hack to get the parent path + val hybridFileParcelable = mainFragment + .elementsList!![1].generateBaseFile() + val hybridFile = HybridFile( + hybridFileParcelable.mode, + hybridFileParcelable.getParent(mainFragment.context) + ) + hybridFile.getParent(mainFragment.context) + } else { + val currentElement = itemsDigested[holder.adapterPosition].elem + currentFileParcelable = currentElement.generateBaseFile() + isCurrentElementDirectory = currentElement.isDirectory + currentElement.desc + } } } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/selection/SelectionPopupMenu.kt b/app/src/main/java/com/amaze/filemanager/ui/selection/SelectionPopupMenu.kt index 054910d4b9..36a63f90fd 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/selection/SelectionPopupMenu.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/selection/SelectionPopupMenu.kt @@ -64,8 +64,11 @@ class SelectionPopupMenu( currentPath, currentContext ) popupMenu.inflate(R.menu.selection_criteria) - if (recyclerAdapter.itemsDigested.size > SIMILARITY_THRESHOLD) { - popupMenu.menu.findItem(R.id.select_similar).isVisible = false + recyclerAdapter.itemsDigested?.let { + itemsDigested -> + if (itemsDigested.size > SIMILARITY_THRESHOLD) { + popupMenu.menu.findItem(R.id.select_similar).isVisible = false + } } popupMenu.setOnMenuItemClickListener(popupMenu) popupMenu.show() From 489bb33cf2d67f98eb5b5d40946c944f1de2e29c Mon Sep 17 00:00:00 2001 From: EmmanuelMess Date: Mon, 1 Nov 2021 22:22:33 -0300 Subject: [PATCH 19/26] Fix NPE on null MainFragmentViewModel --- .../asynchronous/asynctasks/LoadFilesListTask.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java index 536555da1f..26aec2c0b4 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java @@ -44,6 +44,7 @@ import com.amaze.filemanager.filesystem.root.ListFilesCommand; import com.amaze.filemanager.ui.fragments.CloudSheetFragment; import com.amaze.filemanager.ui.fragments.MainFragment; +import com.amaze.filemanager.ui.fragments.data.MainFragmentViewModel; import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.OnAsyncTaskFinished; @@ -69,6 +70,8 @@ public class LoadFilesListTask extends AsyncTask>> { + private static final String TAG = LoadFilesListTask.class.getSimpleName(); + private String path; private WeakReference mainFragmentReference; private WeakReference context; @@ -270,9 +273,14 @@ public LoadFilesListTask( asc = -1; sortby = t - 4; } - Collections.sort( - list, - new FileListSorter(mainFragment.getMainFragmentViewModel().getDsort(), sortby, asc)); + + MainFragmentViewModel viewModel = mainFragment.getMainFragmentViewModel(); + + if (viewModel != null) { + Collections.sort(list, new FileListSorter(viewModel.getDsort(), sortby, asc)); + } else { + Log.e(TAG, "MainFragmentViewModel is null, this is a bug"); + } } return new Pair<>(openmode, list); From 8daa6a893e87743fb1a3cacf869bd3fcb3d68a59 Mon Sep 17 00:00:00 2001 From: EmmanuelMess Date: Tue, 26 Oct 2021 19:13:52 -0300 Subject: [PATCH 20/26] Fix NPE if no UsbManager is available (like on Windows) --- app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt index 05b9b074d8..0c867924b6 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt @@ -195,8 +195,8 @@ object OTGUtil { fun getMassStorageDevicesConnected( context: Context ): List { - val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager - val devices = usbManager.deviceList + val usbManager = context.getSystemService(Context.USB_SERVICE) as? UsbManager + val devices = usbManager?.deviceList ?: mapOf() return devices.mapNotNullTo( ArrayList(), { entry -> From 7b30bae5056cfa2ee40fa42737c7fa0ce925b402 Mon Sep 17 00:00:00 2001 From: EmmanuelMess Date: Sat, 30 Oct 2021 12:03:42 -0300 Subject: [PATCH 21/26] Fix NPE on OTGUtil.getDocumentFiles --- .../main/java/com/amaze/filemanager/utils/OTGUtil.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt index 05b9b074d8..4f8dcc6e9b 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt @@ -98,6 +98,7 @@ object OTGUtil { fileFound: OnFileFound ) { var rootUri = DocumentFile.fromTreeUri(context, rootUriString) + val parts: Array = if (openMode == OpenMode.DOCUMENT_FILE) { path.substringAfter(rootUriString.toString()) .split("/", PATH_SEPARATOR_ENCODED).toTypedArray() @@ -110,11 +111,16 @@ object OTGUtil { if (part == "otg:" || part == "" || part == "content:") continue // iterating through the required path to find the end point - rootUri = rootUri!!.findFile(part) + rootUri = rootUri?.findFile(part) + } + + if (rootUri == null) { + Log.e(TAG, "Null DocumentFile listed!") + return } // we have the end point DocumentFile, list the files inside it and return - for (file in rootUri!!.listFiles()) { + for (file in rootUri.listFiles()) { if (file.exists()) { var size: Long = 0 if (!file.isDirectory) size = file.length() From fddc813f7541a4c72fa048a7bd4761848ed5754d Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Thu, 4 Nov 2021 04:03:10 +0530 Subject: [PATCH 22/26] 2910 Fix comments --- .../filemanager/adapters/RecyclerAdapter.java | 2 +- .../ui/activities/MainActivity.java | 2 +- .../utils/MainActivityActionMode.kt | 593 +++++++++--------- 3 files changed, 304 insertions(+), 293 deletions(-) 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 540076e7c8..0f7e5ccff3 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -249,7 +249,7 @@ public void invalidateActionMode() { if (mainFrag.getMainActivity().getListItemSelected()) { if (mainFrag.getMainActivity().getActionModeHelper().getActionMode() == null) { ActionMode.Callback mActionModeCallback = - mainFrag.getMainActivity().getActionModeHelper().getMActionModeCallback(); + mainFrag.getMainActivity().getActionModeHelper(); mainFrag .getMainActivity() .getActionModeHelper() 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 7f3d35b235..bf867e213a 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 @@ -344,7 +344,7 @@ public void onCreate(final Bundle savedInstanceState) { initialiseFab(); // TODO: 7/12/2017 not init when actionIntent != null mainActivityHelper = new MainActivityHelper(this); - mainActivityActionMode = new MainActivityActionMode(MainActivity.this); + mainActivityActionMode = new MainActivityActionMode(new WeakReference<>(MainActivity.this)); if (CloudSheetFragment.isCloudProviderAvailable(this)) { 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 21cd278347..8c55887bf1 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt @@ -42,28 +42,31 @@ import com.amaze.filemanager.ui.activities.MainActivity import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation import com.amaze.filemanager.ui.selection.SelectionPopupMenu.Companion.invokeSelectionDropdown import java.io.File +import java.lang.ref.WeakReference import java.util.ArrayList -class MainActivityActionMode(val mainActivity: MainActivity) { +class MainActivityActionMode(private val mainActivityReference: WeakReference) : + ActionMode.Callback { var actionModeView: View? = null var actionMode: ActionMode? = null - var mActionModeCallback: ActionMode.Callback = object : ActionMode.Callback { - private fun hideOption(id: Int, menu: Menu) { - val item = menu.findItem(id) - item.isVisible = false - } + private fun hideOption(id: Int, menu: Menu) { + val item = menu.findItem(id) + item.isVisible = false + } - private fun showOption(id: Int, menu: Menu) { - val item = menu.findItem(id) - item.isVisible = true - } + private fun showOption(id: Int, menu: Menu) { + val item = menu.findItem(id) + item.isVisible = true + } - // called when the action mode is created; startActionMode() was called - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - // Inflate a menu resource providing context menu items - val inflater = mode.menuInflater + // called when the action mode is created; startActionMode() was called + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + // Inflate a menu resource providing context menu items + val inflater = mode.menuInflater + mainActivityReference.get()?.let { + mainActivity -> actionModeView = mainActivity.layoutInflater.inflate(R.layout.actionmode, null) mode.customView = actionModeView mainActivity.setPagingEnabled(false) @@ -92,320 +95,328 @@ class MainActivityActionMode(val mainActivity: MainActivity) { if (!mainActivity.drawer.isLocked) { mainActivity.drawer.lockIfNotOnTablet(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) } - return true } + return true + } - /** - * the following method is called each time the action mode is shown. Always called after - * onCreateActionMode, but may be called multiple times if the mode is invalidated. - */ - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - safeLet( - mainActivity.currentMainFragment?.mainFragmentViewModel, - mainActivity.currentMainFragment?.adapter + /** + * the following method is called each time the action mode is shown. Always called after + * onCreateActionMode, but may be called multiple times if the mode is invalidated. + */ + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + safeLet( + mainActivityReference.get(), + mainActivityReference.get()?.currentMainFragment?.mainFragmentViewModel, + mainActivityReference.get()?.currentMainFragment?.adapter + ) { + mainActivity, mainFragmentViewModel, adapter -> + val checkedItems: ArrayList = + mainFragmentViewModel.getCheckedItems() + actionModeView?.setOnClickListener { + invokeSelectionDropdown( + adapter, + actionModeView!!, + mainFragmentViewModel.currentPath!!, + mainActivity + ) + } + val textView: TextView = actionModeView!!.findViewById(R.id.item_count) + textView.text = checkedItems.size.toString() + hideOption(R.id.openmulti, menu) + menu.findItem(R.id.all) + .setTitle( + if (checkedItems.size + == mainFragmentViewModel.folderCount + + mainFragmentViewModel.fileCount + ) R.string.deselect_all else R.string.select_all + ) + if (mainFragmentViewModel.openMode != OpenMode.FILE) { + hideOption(R.id.addshortcut, menu) + hideOption(R.id.compress, menu) + return true + } + if (mainActivity.mReturnIntent && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ) { - mainFragmentViewModel, adapter -> - val checkedItems: ArrayList = - mainFragmentViewModel.getCheckedItems() - actionModeView?.setOnClickListener { - invokeSelectionDropdown( - adapter, - actionModeView!!, - mainFragmentViewModel.currentPath!!, - mainActivity - ) - } - val textView: TextView = actionModeView!!.findViewById(R.id.item_count) - textView.text = checkedItems.size.toString() - hideOption(R.id.openmulti, menu) - menu.findItem(R.id.all) - .setTitle( - if (checkedItems.size - == mainFragmentViewModel.folderCount + - mainFragmentViewModel.fileCount - ) R.string.deselect_all else R.string.select_all - ) - if (mainFragmentViewModel.openMode != OpenMode.FILE) { - hideOption(R.id.addshortcut, menu) - hideOption(R.id.compress, menu) - return true - } - if (mainActivity.mReturnIntent && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN - ) { - showOption(R.id.openmulti, menu) - } - // tv.setText(checkedItems.size()); - if (!mainFragmentViewModel.results) { - hideOption(R.id.openparent, menu) - if (checkedItems.size == 1) { - showOption(R.id.addshortcut, menu) - showOption(R.id.openwith, menu) - showOption(R.id.share, menu) - if (mainFragmentViewModel.getCheckedItems().get(0).isDirectory) { - hideOption(R.id.openwith, menu) + showOption(R.id.openmulti, menu) + } + // tv.setText(checkedItems.size()); + if (!mainFragmentViewModel.results) { + hideOption(R.id.openparent, menu) + if (checkedItems.size == 1) { + showOption(R.id.addshortcut, menu) + showOption(R.id.openwith, menu) + showOption(R.id.share, menu) + if (mainFragmentViewModel.getCheckedItems().get(0).isDirectory) { + hideOption(R.id.openwith, menu) + hideOption(R.id.share, menu) + hideOption(R.id.openmulti, menu) + } + if (mainActivity.mReturnIntent) { + if (Build.VERSION.SDK_INT >= 16) showOption( + R.id.openmulti, + menu + ) + } + } else { + showOption(R.id.share, menu) + if (mainActivity.mReturnIntent && Build.VERSION.SDK_INT >= 16) { + showOption( + R.id.openmulti, + menu + ) + } + for (e in mainFragmentViewModel.getCheckedItems()) { + if (e.isDirectory) { hideOption(R.id.share, menu) hideOption(R.id.openmulti, menu) } - if (mainActivity.mReturnIntent) { - if (Build.VERSION.SDK_INT >= 16) showOption( - R.id.openmulti, - menu - ) - } - } else { - showOption(R.id.share, menu) - if (mainActivity.mReturnIntent && Build.VERSION.SDK_INT >= 16) { - showOption( - R.id.openmulti, - menu - ) - } - for (e in mainFragmentViewModel.getCheckedItems()) { - if (e.isDirectory) { - hideOption(R.id.share, menu) - hideOption(R.id.openmulti, menu) - } - } + } + hideOption(R.id.openwith, menu) + hideOption(R.id.addshortcut, menu) + } + } else { + if (checkedItems.size == 1) { + showOption(R.id.addshortcut, menu) + showOption(R.id.openparent, menu) + showOption(R.id.openwith, menu) + showOption(R.id.share, menu) + if (mainFragmentViewModel.getCheckedItems()[0].isDirectory) { hideOption(R.id.openwith, menu) - hideOption(R.id.addshortcut, menu) + hideOption(R.id.share, menu) + hideOption(R.id.openmulti, menu) + } + if (mainActivity.mReturnIntent && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + ) { + showOption(R.id.openmulti, menu) } } else { - if (checkedItems.size == 1) { - showOption(R.id.addshortcut, menu) - showOption(R.id.openparent, menu) - showOption(R.id.openwith, menu) - showOption(R.id.share, menu) - if (mainFragmentViewModel.getCheckedItems()[0].isDirectory) { - hideOption(R.id.openwith, menu) + hideOption(R.id.openparent, menu) + hideOption(R.id.addshortcut, menu) + if (mainActivity.mReturnIntent && Build.VERSION.SDK_INT >= 16) { + showOption( + R.id.openmulti, + menu + ) + } + for (e in mainFragmentViewModel.getCheckedItems()) { + if (e.isDirectory) { hideOption(R.id.share, menu) hideOption(R.id.openmulti, menu) } - if (mainActivity.mReturnIntent && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN - ) { - showOption(R.id.openmulti, menu) - } - } else { - hideOption(R.id.openparent, menu) - hideOption(R.id.addshortcut, menu) - if (mainActivity.mReturnIntent && Build.VERSION.SDK_INT >= 16) { - showOption( - R.id.openmulti, - menu - ) - } - for (e in mainFragmentViewModel.getCheckedItems()) { - if (e.isDirectory) { - hideOption(R.id.share, menu) - hideOption(R.id.openmulti, menu) - } - } - hideOption(R.id.openwith, menu) } - } - if (mainFragmentViewModel.openMode != OpenMode.FILE) { - hideOption(R.id.addshortcut, menu) - hideOption(R.id.compress, menu) - hideOption(R.id.hide, menu) - hideOption(R.id.addshortcut, menu) + hideOption(R.id.openwith, menu) } } - return true // Return false if nothing is done + if (mainFragmentViewModel.openMode != OpenMode.FILE) { + hideOption(R.id.addshortcut, menu) + hideOption(R.id.compress, menu) + hideOption(R.id.hide, menu) + hideOption(R.id.addshortcut, menu) + } } + return true // Return false if nothing is done + } - // called when the user selects a contextual menu item - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - mainActivity.currentMainFragment?.computeScroll() - mainActivity.currentMainFragment?.mainFragmentViewModel?.getCheckedItems()?.also { - checkedItems -> - return when (item.itemId) { - R.id.openmulti -> { - val intent_result = Intent(Intent.ACTION_SEND_MULTIPLE) - val resulturis = ArrayList() - for (element in checkedItems) { - val baseFile = element.generateBaseFile() - val resultUri = Utils.getUriForBaseFile(mainActivity, baseFile) - if (resultUri != null) { - resulturis.add(resultUri) - } + // called when the user selects a contextual menu item + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + mainActivityReference.get()?.currentMainFragment?.computeScroll() + safeLet( + mainActivityReference.get(), + mainActivityReference + .get()?.currentMainFragment?.mainFragmentViewModel?.getCheckedItems() + ) { + mainActivity, checkedItems -> + return when (item.itemId) { + R.id.openmulti -> { + val intent_result = Intent(Intent.ACTION_SEND_MULTIPLE) + val resulturis = ArrayList() + for (element in checkedItems) { + val baseFile = element.generateBaseFile() + val resultUri = Utils.getUriForBaseFile(mainActivity, baseFile) + if (resultUri != null) { + resulturis.add(resultUri) } - intent_result.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - mainActivity.setResult(FragmentActivity.RESULT_OK, intent_result) - intent_result.putParcelableArrayListExtra( - Intent.EXTRA_STREAM, - resulturis - ) - mainActivity.finish() - // mode.finish(); - true } - R.id.about -> { - val x = checkedItems[0] - mainActivity.currentMainFragment?.also { - GeneralDialogCreation.showPropertiesDialogWithPermissions( - x.generateBaseFile(), - x.permissions, - mainActivity, - it, - mainActivity.isRootExplorer, - mainActivity.utilsProvider.appTheme - ) - } - mode.finish() - true - } - R.id.delete -> { - GeneralDialogCreation.deleteFilesDialog( - mainActivity, + intent_result.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + mainActivity.setResult(FragmentActivity.RESULT_OK, intent_result) + intent_result.putParcelableArrayListExtra( + Intent.EXTRA_STREAM, + resulturis + ) + mainActivity.finish() + // mode.finish(); + true + } + R.id.about -> { + val x = checkedItems[0] + mainActivity.currentMainFragment?.also { + GeneralDialogCreation.showPropertiesDialogWithPermissions( + x.generateBaseFile(), + x.permissions, mainActivity, - checkedItems, + it, + mainActivity.isRootExplorer, mainActivity.utilsProvider.appTheme ) - true - } - R.id.share -> { - val arrayList = ArrayList() - for (e in checkedItems) { - arrayList.add(File(e.desc)) - } - if (arrayList.size > 100) Toast.makeText( - mainActivity, - mainActivity.resources.getString(R.string.share_limit), - Toast.LENGTH_SHORT - ) - .show() else { - mainActivity.currentMainFragment?.mainFragmentViewModel?.also { - mainFragmentViewModel -> - when (mainFragmentViewModel.listElements?.get(0)?.mode) { - OpenMode.DROPBOX, OpenMode.BOX, OpenMode.GDRIVE, - OpenMode.ONEDRIVE -> - mainFragmentViewModel.listElements?.also { - FileUtils.shareCloudFile( - it[0].desc, - it[0].mode, - mainActivity - ) - } - else -> FileUtils.shareFiles( - arrayList, - mainActivity, - mainActivity.utilsProvider.appTheme, - mainFragmentViewModel.accentColor - ) - } - } - } - true } - R.id.openparent -> { - mainActivity.currentMainFragment?.loadlist( - File(checkedItems[0].desc).parent, - false, OpenMode.FILE - ) - - true + mode.finish() + true + } + R.id.delete -> { + GeneralDialogCreation.deleteFilesDialog( + mainActivity, + mainActivity, + checkedItems, + mainActivity.utilsProvider.appTheme + ) + true + } + R.id.share -> { + val arrayList = ArrayList() + for (e in checkedItems) { + arrayList.add(File(e.desc)) } - R.id.all -> { - safeLet( - mainActivity.currentMainFragment?.mainFragmentViewModel, - mainActivity.currentMainFragment?.adapter - ) { - mainFragmentViewModel, adapter -> - if (adapter.areAllChecked(mainFragmentViewModel.currentPath)) { - adapter.toggleChecked( - false, - mainFragmentViewModel.currentPath - ) - item.setTitle(R.string.select_all) - } else { - adapter.toggleChecked( - true, - mainFragmentViewModel.currentPath + if (arrayList.size > 100) Toast.makeText( + mainActivity, + mainActivity.resources.getString(R.string.share_limit), + Toast.LENGTH_SHORT + ) + .show() else { + mainActivity.currentMainFragment?.mainFragmentViewModel?.also { + mainFragmentViewModel -> + when (mainFragmentViewModel.listElements?.get(0)?.mode) { + OpenMode.DROPBOX, OpenMode.BOX, OpenMode.GDRIVE, + OpenMode.ONEDRIVE -> + mainFragmentViewModel.listElements?.also { + FileUtils.shareCloudFile( + it[0].desc, + it[0].mode, + mainActivity + ) + } + else -> FileUtils.shareFiles( + arrayList, + mainActivity, + mainActivity.utilsProvider.appTheme, + mainFragmentViewModel.accentColor ) - item.setTitle(R.string.deselect_all) } } - mode.invalidate() - true - } - R.id.rename -> { - val f: HybridFileParcelable = checkedItems[0].generateBaseFile() - mainActivity.currentMainFragment?.rename(f) - mode.finish() - true } - R.id.hide -> { - var i1 = 0 - while (i1 < checkedItems.size) { - mainActivity.currentMainFragment?.hide(checkedItems[i1].desc) - i1++ + true + } + R.id.openparent -> { + mainActivity.currentMainFragment?.loadlist( + File(checkedItems[0].desc).parent, + false, OpenMode.FILE + ) + + true + } + R.id.all -> { + safeLet( + mainActivity.currentMainFragment?.mainFragmentViewModel, + mainActivity.currentMainFragment?.adapter + ) { + mainFragmentViewModel, adapter -> + if (adapter.areAllChecked(mainFragmentViewModel.currentPath)) { + adapter.toggleChecked( + false, + mainFragmentViewModel.currentPath + ) + item.setTitle(R.string.select_all) + } else { + adapter.toggleChecked( + true, + mainFragmentViewModel.currentPath + ) + item.setTitle(R.string.deselect_all) } - mainActivity.currentMainFragment?.updateList() - mode.finish() - true - } - R.id.ex -> { - mainActivity.mainActivityHelper.extractFile(File(checkedItems[0].desc)) - mode.finish() - true } - R.id.cpy, R.id.cut -> { - val copies = arrayOfNulls(checkedItems.size) - var i = 0 - while (i < checkedItems.size) { - copies[i] = checkedItems[i].generateBaseFile() - i++ - } - val op = - if (item.itemId == R.id.cpy) PasteHelper.OPERATION_COPY - else PasteHelper.OPERATION_CUT - // Making sure we don't cause an IllegalArgumentException - // when passing copies to PasteHelper - if (copies.isNotEmpty()) { - val pasteHelper = PasteHelper(mainActivity, op, copies) - mainActivity.paste = pasteHelper - } - mode.finish() - true + mode.invalidate() + true + } + R.id.rename -> { + val f: HybridFileParcelable = checkedItems[0].generateBaseFile() + mainActivity.currentMainFragment?.rename(f) + mode.finish() + true + } + R.id.hide -> { + var i1 = 0 + while (i1 < checkedItems.size) { + mainActivity.currentMainFragment?.hide(checkedItems[i1].desc) + i1++ } - R.id.compress -> { - val copies1 = ArrayList() - var i4 = 0 - while (i4 < checkedItems.size) { - copies1.add(checkedItems[i4].generateBaseFile()) - i4++ - } - GeneralDialogCreation.showCompressDialog( - mainActivity, copies1, - mainActivity.currentMainFragment?.mainFragmentViewModel?.currentPath - ) - mode.finish() - true + mainActivity.currentMainFragment?.updateList() + mode.finish() + true + } + R.id.ex -> { + mainActivity.mainActivityHelper.extractFile(File(checkedItems[0].desc)) + mode.finish() + true + } + R.id.cpy, R.id.cut -> { + val copies = arrayOfNulls(checkedItems.size) + var i = 0 + while (i < checkedItems.size) { + copies[i] = checkedItems[i].generateBaseFile() + i++ } - R.id.openwith -> { - FileUtils.openFile( - File(checkedItems[0].desc), mainActivity, mainActivity.prefs - ) - true + val op = + if (item.itemId == R.id.cpy) PasteHelper.OPERATION_COPY + else PasteHelper.OPERATION_CUT + // Making sure we don't cause an IllegalArgumentException + // when passing copies to PasteHelper + if (copies.isNotEmpty()) { + val pasteHelper = PasteHelper(mainActivity, op, copies) + mainActivity.paste = pasteHelper } - R.id.addshortcut -> { - Utils.addShortcut( - mainActivity, mainActivity.componentName, - checkedItems[0] - ) - mode.finish() - true + mode.finish() + true + } + R.id.compress -> { + val copies1 = ArrayList() + var i4 = 0 + while (i4 < checkedItems.size) { + copies1.add(checkedItems[i4].generateBaseFile()) + i4++ } - else -> false + GeneralDialogCreation.showCompressDialog( + mainActivity, copies1, + mainActivity.currentMainFragment?.mainFragmentViewModel?.currentPath + ) + mode.finish() + true } + R.id.openwith -> { + FileUtils.openFile( + File(checkedItems[0].desc), mainActivity, mainActivity.prefs + ) + true + } + R.id.addshortcut -> { + Utils.addShortcut( + mainActivity, mainActivity.componentName, + checkedItems[0] + ) + mode.finish() + true + } + else -> false } - return false } + return false + } - // called when the user exits the action mode - override fun onDestroyActionMode(mode: ActionMode) { - actionMode = null + // called when the user exits the action mode + override fun onDestroyActionMode(mode: ActionMode) { + actionMode = null + mainActivityReference.get()?.let { + mainActivity -> mainActivity.listItemSelected = false // translates the drawer content up @@ -441,7 +452,7 @@ class MainActivityActionMode(val mainActivity: MainActivity) { * Finishes the action mode */ fun disableActionMode() { - mainActivity.listItemSelected = false + mainActivityReference.get()?.listItemSelected = false actionMode?.finish() actionMode = null } From 3af6c67615b5699a252323f3098f7ed7b854c250 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 6 Nov 2021 22:45:54 +0800 Subject: [PATCH 23/26] Update billing library to 4.0.0 Fixes #2985. Also added convenience method to run a Callable in main thread. --- .../filemanager/application/AppConfig.java | 19 +++++++--- .../com/amaze/filemanager/utils/Billing.java | 36 ++++++++++++------- build.gradle | 2 +- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java index 9491204bab..6f7f63399b 100644 --- a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java +++ b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java @@ -21,6 +21,7 @@ package com.amaze.filemanager.application; import java.lang.ref.WeakReference; +import java.util.concurrent.Callable; import org.acra.ACRA; import org.acra.annotation.AcraCore; @@ -65,7 +66,7 @@ reportSenderFactoryClasses = AcraReportSenderFactory.class) public class AppConfig extends GlideApplication { - public static final String TAG = AppConfig.class.getSimpleName(); + private static final String TAG = AppConfig.class.getSimpleName(); private UtilitiesProvider utilsProvider; private RequestQueue requestQueue; @@ -175,14 +176,24 @@ public static void toast(Context context, String message) { } /** - * Run a runnable in the main application thread + * Run a {@link Runnable} in the main application thread * - * @param r Runnable to run + * @param r {@link Runnable} to run */ - public void runInApplicationThread(Runnable r) { + public void runInApplicationThread(@NonNull Runnable r) { Completable.fromRunnable(r).subscribeOn(AndroidSchedulers.mainThread()).subscribe(); } + /** + * Convenience method to run a {@link Callable} in the main application thread. Use when the + * callable's return value is not processed. + * + * @param c {@link Callable} to run + */ + public void runInApplicationThread(@NonNull Callable c) { + Completable.fromCallable(c).subscribeOn(AndroidSchedulers.mainThread()).subscribe(); + } + public static synchronized AppConfig getInstance() { return instance; } diff --git a/app/src/play/java/com/amaze/filemanager/utils/Billing.java b/app/src/play/java/com/amaze/filemanager/utils/Billing.java index d7e910a026..c7cb31e53a 100644 --- a/app/src/play/java/com/amaze/filemanager/utils/Billing.java +++ b/app/src/play/java/com/amaze/filemanager/utils/Billing.java @@ -26,6 +26,8 @@ import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.holders.DonationViewHolder; +import com.amaze.filemanager.application.AppConfig; +import com.amaze.filemanager.databinding.AdapterDonationBinding; import com.amaze.filemanager.ui.activities.superclasses.BasicActivity; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingClientStateListener; @@ -38,7 +40,6 @@ import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsParams; -import android.content.Context; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -54,7 +55,6 @@ public class Billing extends RecyclerView.Adapter private BasicActivity activity; private List skuList; - private LayoutInflater layoutInflater; private List skuDetails; // create new donations client @@ -62,7 +62,7 @@ public class Billing extends RecyclerView.Adapter /** True if billing service is connected now. */ private boolean isServiceConnected; - public Billing(BasicActivity activity) { + public Billing(@NonNull BasicActivity activity) { this.activity = activity; skuList = new ArrayList<>(); @@ -71,8 +71,6 @@ public Billing(BasicActivity activity) { skuList.add("donations_3"); skuList.add("donations_4"); - layoutInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - billingClient = BillingClient.newBuilder(activity).setListener(this).enablePendingPurchases().build(); initiatePurchaseFlow(); @@ -128,7 +126,8 @@ private void popProductsList(BillingResult response, List skuDetails @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View rootView = layoutInflater.inflate(R.layout.adapter_donation, parent, false); + View rootView = + AdapterDonationBinding.inflate(LayoutInflater.from(this.activity), parent, false).getRoot(); return new DonationViewHolder(rootView); } @@ -201,7 +200,7 @@ private void startServiceConnection(final Runnable executeOnSuccess) { public void onBillingSetupFinished(BillingResult billingResponse) { Log.d( Billing.this.getClass().getSimpleName(), - "Setup finished. Response code: " + billingResponse); + "Setup finished. Response code: " + billingResponse.getResponseCode()); if (billingResponse.getResponseCode() == BillingClient.BillingResponseCode.OK) { isServiceConnected = true; @@ -226,11 +225,22 @@ public void destroyBillingInstance() { } private void showPaymentsDialog(final BasicActivity context) { - final MaterialDialog.Builder builder = new MaterialDialog.Builder(context); - builder.title(R.string.donate); - builder.adapter(this, null); - builder.theme(context.getAppTheme().getMaterialDialogTheme()); - builder.cancelListener(dialog -> purchaseProduct.purchaseCancel()); - builder.show(); + /* + * As of Billing library 4.0, all callbacks are running on background thread. + * Need to use AppConfig.runInApplicationThread() for UI interactions + * + * + */ + AppConfig.getInstance() + .runInApplicationThread( + () -> { + final MaterialDialog.Builder builder = new MaterialDialog.Builder(context); + builder.title(R.string.donate); + builder.adapter(this, null); + builder.theme(context.getAppTheme().getMaterialDialogTheme()); + builder.cancelListener(dialog -> purchaseProduct.purchaseCancel()); + builder.show(); + return null; + }); } } diff --git a/build.gradle b/build.gradle index e1b1ff85fd..38eb493f01 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { slf4jVersion = "1.7.25" mockitoVersion = "3.9.0" mockitoKotlinVersion = "3.2.0" - androidBillingVersion = "2.1.0" + androidBillingVersion = "4.0.0" junrarVersion = "7.4.0" zip4jVersion = "2.6.4" espressoVersion = "3.3.0" From e3d47c81f76bff8d071ad9124b185895ecd5e0d2 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sun, 7 Nov 2021 22:53:50 +0800 Subject: [PATCH 24/26] Changes per PR feedback - protection against crash for running debug build - Add emptiness check of skuDetailsList - Toast user of empty product list, and indicate in log if build is DEBUG variants --- app/src/main/res/values/strings.xml | 1 + .../com/amaze/filemanager/utils/Billing.java | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98e1a29b4d..0d2c281a1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -728,5 +728,6 @@ You only need to do this once, until the next time you select a new location for Select by type Select by date Select similar + Error fetching product list from Google Play. diff --git a/app/src/play/java/com/amaze/filemanager/utils/Billing.java b/app/src/play/java/com/amaze/filemanager/utils/Billing.java index c7cb31e53a..bf15f8702c 100644 --- a/app/src/play/java/com/amaze/filemanager/utils/Billing.java +++ b/app/src/play/java/com/amaze/filemanager/utils/Billing.java @@ -24,6 +24,7 @@ import java.util.List; import com.afollestad.materialdialogs.MaterialDialog; +import com.amaze.filemanager.BuildConfig; import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.holders.DonationViewHolder; import com.amaze.filemanager.application.AppConfig; @@ -53,6 +54,8 @@ public class Billing extends RecyclerView.Adapter implements PurchasesUpdatedListener { + private static final String TAG = Billing.class.getSimpleName(); + private BasicActivity activity; private List skuList; private List skuDetails; @@ -101,9 +104,18 @@ private void initiatePurchaseFlow() { billingClient.querySkuDetailsAsync( params.build(), (responseCode, skuDetailsList) -> { - // Successfully fetched product details - skuDetails = skuDetailsList; - popProductsList(responseCode, skuDetailsList); + if (skuDetailsList != null && skuDetailsList.size() > 0) { + // Successfully fetched product details + skuDetails = skuDetailsList; + popProductsList(responseCode, skuDetailsList); + } else { + AppConfig.toast(activity, R.string.error_fetching_google_play_product_list); + if (BuildConfig.DEBUG) { + Log.w( + TAG, + "Error fetching product list - looks like you are running a DEBUG build."); + } + } }); }; @@ -133,7 +145,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - if (holder instanceof DonationViewHolder) { + if (holder instanceof DonationViewHolder && skuDetails.size() > 0) { String titleRaw = skuDetails.get(position).getTitle(); ((DonationViewHolder) holder) .TITLE.setText(titleRaw.subSequence(0, titleRaw.lastIndexOf("("))); @@ -198,10 +210,7 @@ private void startServiceConnection(final Runnable executeOnSuccess) { new BillingClientStateListener() { @Override public void onBillingSetupFinished(BillingResult billingResponse) { - Log.d( - Billing.this.getClass().getSimpleName(), - "Setup finished. Response code: " + billingResponse.getResponseCode()); - + Log.d(TAG, "Setup finished. Response code: " + billingResponse.getResponseCode()); if (billingResponse.getResponseCode() == BillingClient.BillingResponseCode.OK) { isServiceConnected = true; if (executeOnSuccess != null) { From 0da4228fa6eef4f5b7621ca0a6198738365c21ce Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Tue, 9 Nov 2021 03:04:01 +0530 Subject: [PATCH 25/26] Bump version number --- app/build.gradle | 6 ++--- .../DocumentFileNotFoundException.kt | 26 +++++++++++++++++++ .../com/amaze/filemanager/utils/OTGUtil.kt | 7 ++--- app/src/main/res/values/translators.xml | 2 +- 4 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/amaze/filemanager/exceptions/DocumentFileNotFoundException.kt diff --git a/app/build.gradle b/app/build.gradle index d0bdf06803..7ef2cac70c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,9 +27,9 @@ android { defaultConfig { applicationId "com.amaze.filemanager" minSdkVersion 14 - targetSdkVersion 29 - versionCode 99 - versionName "3.6.5" + targetSdkVersion 30 + versionCode 100 + versionName "3.6.6" multiDexEnabled true vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/com/amaze/filemanager/exceptions/DocumentFileNotFoundException.kt b/app/src/main/java/com/amaze/filemanager/exceptions/DocumentFileNotFoundException.kt new file mode 100644 index 0000000000..ac0b5fccc3 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/exceptions/DocumentFileNotFoundException.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2014-2021 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.exceptions + +import android.net.Uri + +class DocumentFileNotFoundException(rootUri: Uri, path: String) : + RuntimeException("Root uri: %s and path %s".format(rootUri.path, path)) diff --git a/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt index 6dc29625a6..5cab338b6a 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt @@ -31,6 +31,7 @@ import android.provider.DocumentsContract import android.util.Log import androidx.annotation.RequiresApi import androidx.documentfile.provider.DocumentFile +import com.amaze.filemanager.exceptions.DocumentFileNotFoundException import com.amaze.filemanager.file_operations.filesystem.OpenMode import com.amaze.filemanager.file_operations.filesystem.usb.SingletonUsbOtg import com.amaze.filemanager.file_operations.filesystem.usb.UsbOtgRepresentation @@ -115,8 +116,7 @@ object OTGUtil { } if (rootUri == null) { - Log.e(TAG, "Null DocumentFile listed!") - return + throw DocumentFileNotFoundException(rootUriString, path) } // we have the end point DocumentFile, list the files inside it and return @@ -167,7 +167,8 @@ object OTGUtil { createRecursive: Boolean ): DocumentFile? { // start with root of SD card and then parse through document tree. - var retval = DocumentFile.fromTreeUri(context, rootUri) + var retval: DocumentFile? = DocumentFile.fromTreeUri(context, rootUri) + ?: throw DocumentFileNotFoundException(rootUri, path) val parts: Array = if (openMode == OpenMode.DOCUMENT_FILE) { path.substringAfter(rootUri.toString()) .split("/", PATH_SEPARATOR_ENCODED).toTypedArray() diff --git a/app/src/main/res/values/translators.xml b/app/src/main/res/values/translators.xml index 181abae3bf..39a80cba50 100644 --- a/app/src/main/res/values/translators.xml +++ b/app/src/main/res/values/translators.xml @@ -42,7 +42,7 @@ ngoisaosang Naofumi Fukue Kuralarasi for StarsSoft - v3.6.5 + v3.6.6 Arpit Khurana Vishal Nehra Emmanuel Messulam From 87dc706cc8885e8371f17e082ed5ed87f8c4bf0d Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Tue, 9 Nov 2021 04:04:03 +0530 Subject: [PATCH 26/26] Bump version number --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7ef2cac70c..552ee1648b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'com.hiya.jacoco-android' apply plugin: 'com.akaita.android.easylauncher' android { - compileSdkVersion 29 + compileSdkVersion 30 dexOptions { jumboMode = true @@ -28,7 +28,7 @@ android { applicationId "com.amaze.filemanager" minSdkVersion 14 targetSdkVersion 30 - versionCode 100 + versionCode 104 versionName "3.6.6" multiDexEnabled true