From 4c690f59599842a0e7f78bdc88fee9ae8020e957 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 8 Oct 2022 09:44:49 +0530 Subject: [PATCH 001/384] initial ui --- .../BackupPrefsFragment.kt | 52 +++++++++++++++++++ .../PreferencesConstants.kt | 4 ++ .../preferencefragments/PrefsFragment.kt | 6 +++ .../ic_preference_settings_backup_restore.xml | 14 +++++ .../drawable/ic_settings_backup_restore.xml | 10 ++++ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 6 +++ app/src/main/res/xml/backup_prefs.xml | 19 +++++++ app/src/main/res/xml/preferences.xml | 7 +++ 9 files changed, 119 insertions(+) create mode 100644 app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt create mode 100644 app/src/main/res/drawable/ic_preference_settings_backup_restore.xml create mode 100644 app/src/main/res/drawable/ic_settings_backup_restore.xml create mode 100644 app/src/main/res/xml/backup_prefs.xml diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt new file mode 100644 index 0000000000..c2845dee78 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -0,0 +1,52 @@ +/* + * 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.ui.fragments.preferencefragments + +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.Preference.OnPreferenceClickListener +import com.amaze.filemanager.R + +class BackupPrefsFragment : BasePrefsFragment() { + override val title = R.string.backup + + private val onExportPrefClick = OnPreferenceClickListener { + + true + } + + private val onImportPrefClick = OnPreferenceClickListener { + + true + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.backup_prefs, rootKey) + + findPreference( + PreferencesConstants.PREFERENCE_EXPORT_SETTINGS + )?.onPreferenceClickListener = onExportPrefClick + + findPreference( + PreferencesConstants.PREFERENCE_IMPORT_SETTINGS + )?.onPreferenceClickListener = onImportPrefClick + } +} diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt index 62af3ebe4a..fce59271cd 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt @@ -87,6 +87,10 @@ object PreferencesConstants { const val ENCRYPT_PASSWORD_MASTER = "master" const val PREFERENCE_CRYPT_WARNING_REMEMBER_DEFAULT = false + // backup_prefs.xml + const val PREFERENCE_EXPORT_SETTINGS = "export_settings" + const val PREFERENCE_IMPORT_SETTINGS = "import_settings" + // others const val PREFERENCE_CURRENT_TAB = "" const val PREFERENCE_BOOKMARKS_ADDED = "books_added" diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PrefsFragment.kt index 400839c7cd..049b8f59e2 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PrefsFragment.kt @@ -59,6 +59,12 @@ class PrefsFragment : BasePrefsFragment() { true } + findPreference("backup")?.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + activity.pushFragment(BackupPrefsFragment()) + true + } + findPreference("about")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { startActivity(Intent(activity, AboutActivity::class.java)) diff --git a/app/src/main/res/drawable/ic_preference_settings_backup_restore.xml b/app/src/main/res/drawable/ic_preference_settings_backup_restore.xml new file mode 100644 index 0000000000..237319909d --- /dev/null +++ b/app/src/main/res/drawable/ic_preference_settings_backup_restore.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_backup_restore.xml b/app/src/main/res/drawable/ic_settings_backup_restore.xml new file mode 100644 index 0000000000..8ff868a205 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_backup_restore.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 744b4fa36f..a76ae45d01 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -126,6 +126,7 @@ #f9a825 #009688 #da4336 + @color/accent_deep_orange #757575 #262a3e diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 466d51bfc0..fab6fdf759 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -759,6 +759,12 @@ You only need to do this once, until the next time you select a new location for Control what information to show, customize the sidebar, and toggle UI preferences Modify behaviors such as advanced search and whether to open files as a new task Set up password and encryption + Export/Import Amaze settings + + Export + Export settings + Import + Import settings Files recently accessed through Amaze Recently created or modified files diff --git a/app/src/main/res/xml/backup_prefs.xml b/app/src/main/res/xml/backup_prefs.xml new file mode 100644 index 0000000000..64cde4e451 --- /dev/null +++ b/app/src/main/res/xml/backup_prefs.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b78c38a5ae..03fd6bd6c6 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -47,6 +47,13 @@ app:key="security" app:summary="@string/security_summary" app:title="@string/security" /> + Date: Sat, 8 Oct 2022 11:24:03 +0530 Subject: [PATCH 002/384] export --- app/build.gradle | 3 ++ .../BackupPrefsFragment.kt | 50 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 3 files changed, 55 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 0560c66048..04dd11d408 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,6 +242,9 @@ dependencies { implementation "ch.acra:acra-core:5.7.0" runtimeOnly 'org.slf4j:slf4j-api:1.7.32' runtimeOnly "com.github.tony19:logback-android:$logbackAndroidVersion" + + implementation 'com.google.code.gson:gson:2.9.1' + } configurations.all { diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index c2845dee78..33867a4f0b 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -20,16 +20,66 @@ package com.amaze.filemanager.ui.fragments.preferencefragments +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.util.Log +import android.widget.Toast import androidx.preference.Preference import androidx.preference.Preference.OnPreferenceClickListener +import androidx.preference.PreferenceManager import com.amaze.filemanager.R +import com.amaze.filemanager.ui.activities.MainActivity +import com.google.gson.Gson +import java.io.File +import java.io.FileWriter +import java.io.IOException class BackupPrefsFragment : BasePrefsFragment() { + + private val TAG: String = BasePrefsFragment::class.java.simpleName + override val title = R.string.backup private val onExportPrefClick = OnPreferenceClickListener { + val map: Map<*, *> = PreferenceManager.getDefaultSharedPreferences(getActivity()).all + + val gsonString: String = Gson().toJson(map) + + try { + + val file = File(context?.cacheDir?.absolutePath + File.separator + "amaze_backup.json") + + val fileWriter = FileWriter(file) + + fileWriter.append(gsonString) + + Log.e(TAG, "wrote export to :${file.absolutePath}") + + fileWriter.flush() + fileWriter.close() + + Toast.makeText( + context, + getString(R.string.select_save_location), + Toast.LENGTH_SHORT + ) + .show() + + val intent = Intent(context, MainActivity::class.java) + + intent.action = Intent.ACTION_SEND + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)) + + startActivity(intent) + } catch (e: IOException) { + Toast.makeText(context, getString(R.string.exporting_failed), Toast.LENGTH_SHORT).show() + e.printStackTrace() + } + true } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fab6fdf759..c1427126ff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -795,5 +795,7 @@ You only need to do this once, until the next time you select a new location for FTP Secure FTP Unavailable + + Exporting failed From f9b54bf283d735e8daedf5857f0097e6dedeaf37 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 8 Oct 2022 21:31:04 +0530 Subject: [PATCH 003/384] import --- .../BackupPrefsFragment.kt | 92 +++++++++++++++++-- app/src/main/res/values/strings.xml | 2 + 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index 33867a4f0b..8d9578bde7 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -20,7 +20,9 @@ package com.amaze.filemanager.ui.fragments.preferencefragments +import android.app.Activity import android.content.Intent +import android.content.SharedPreferences import android.net.Uri import android.os.Bundle import android.util.Log @@ -29,21 +31,22 @@ import androidx.preference.Preference import androidx.preference.Preference.OnPreferenceClickListener import androidx.preference.PreferenceManager import com.amaze.filemanager.R +import com.amaze.filemanager.TagsHelper import com.amaze.filemanager.ui.activities.MainActivity import com.google.gson.Gson -import java.io.File -import java.io.FileWriter -import java.io.IOException +import com.google.gson.reflect.TypeToken +import java.io.* class BackupPrefsFragment : BasePrefsFragment() { - private val TAG: String = BasePrefsFragment::class.java.simpleName + private val TAG: String = TagsHelper.getTag(BasePrefsFragment::class.java) + private val IMPORT_BACKUP_FILE: Int = 2 override val title = R.string.backup private val onExportPrefClick = OnPreferenceClickListener { - val map: Map<*, *> = PreferenceManager.getDefaultSharedPreferences(getActivity()).all + val map: Map = PreferenceManager.getDefaultSharedPreferences(getActivity()).all val gsonString: String = Gson().toJson(map) @@ -55,7 +58,7 @@ class BackupPrefsFragment : BasePrefsFragment() { fileWriter.append(gsonString) - Log.e(TAG, "wrote export to :${file.absolutePath}") + Log.e(TAG, "wrote export to: ${file.absolutePath}") fileWriter.flush() fileWriter.close() @@ -64,8 +67,7 @@ class BackupPrefsFragment : BasePrefsFragment() { context, getString(R.string.select_save_location), Toast.LENGTH_SHORT - ) - .show() + ).show() val intent = Intent(context, MainActivity::class.java) @@ -85,9 +87,83 @@ class BackupPrefsFragment : BasePrefsFragment() { private val onImportPrefClick = OnPreferenceClickListener { + var intent = Intent(context, MainActivity::class.java) + intent.action = Intent.ACTION_GET_CONTENT + intent.type = "file/json" + intent = Intent.createChooser(intent, "Choose backup file") + startActivityForResult(intent, IMPORT_BACKUP_FILE) + true } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == IMPORT_BACKUP_FILE && resultCode == Activity.RESULT_OK) { + if (data != null && data.data != null) { + val uri = data.data + + Log.e(TAG, "read import file: $uri") + + try { + val inputStream = uri?.let { + context?.contentResolver?.openInputStream(it) + } + + val bufferedReader = BufferedReader(InputStreamReader(inputStream)) + val stringBuilder = StringBuilder() + + var line: String? + while (bufferedReader.readLine().also { line = it } != null) + stringBuilder.append(line).append('\n') + + val type = object : TypeToken>() {}.type + + val map: Map = Gson().fromJson( + stringBuilder.toString(), + type + ) + + val editor: SharedPreferences.Editor? = + PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() + + for ((key, value) in map) try { + if (value is Boolean) editor?.putBoolean(key, value) + if (value is Float) editor?.putFloat(key, value) + if (value is Int) editor?.putInt(key, value) + if (value is Long) editor?.putLong(key, value) + if (value is String) editor?.putString(key, value) + if (value is Set<*>) editor?.putStringSet(key, value as Set) + } catch (e: java.lang.ClassCastException) { + e.printStackTrace() + } + + editor?.apply() + + Toast.makeText( + context, + getString(R.string.importing_completed), + Toast.LENGTH_SHORT + ).show() + + startActivity( + Intent( + context, + MainActivity::class.java + ) + ) // restart Amaze for changes to take effect + } catch (e: IOException) { + Toast.makeText( + context, + getString(R.string.importing_failed), + Toast.LENGTH_SHORT + ).show() + e.printStackTrace() + } + } + } + } + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.backup_prefs, rootKey) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1427126ff..571b87fce0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,5 +797,7 @@ You only need to do this once, until the next time you select a new location for Unavailable Exporting failed + Importing failed + Importing completed From fbeafb8fafe5d14a53cb83e71b4276d127eea69e Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 8 Oct 2022 22:00:05 +0530 Subject: [PATCH 004/384] restrict intent to json files only --- .../BackupPrefsFragment.kt | 130 ++++++++++-------- app/src/main/res/values/strings.xml | 1 + 2 files changed, 76 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index 8d9578bde7..8b9a0f09e5 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -24,6 +24,7 @@ import android.app.Activity import android.content.Intent import android.content.SharedPreferences import android.net.Uri +import android.os.Build import android.os.Bundle import android.util.Log import android.widget.Toast @@ -87,11 +88,27 @@ class BackupPrefsFragment : BasePrefsFragment() { private val onImportPrefClick = OnPreferenceClickListener { - var intent = Intent(context, MainActivity::class.java) - intent.action = Intent.ACTION_GET_CONTENT - intent.type = "file/json" - intent = Intent.createChooser(intent, "Choose backup file") - startActivityForResult(intent, IMPORT_BACKUP_FILE) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + startActivityForResult( + Intent(Intent.ACTION_OPEN_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*") + .putExtra( + Intent.EXTRA_MIME_TYPES, + arrayOf("application/json") + ), + IMPORT_BACKUP_FILE + ) + } else { + startActivityForResult( + Intent.createChooser( + Intent(Intent.ACTION_GET_CONTENT) + .setType("application/json"), + "Choose backup file" + ), + IMPORT_BACKUP_FILE + ) + } true } @@ -99,68 +116,71 @@ class BackupPrefsFragment : BasePrefsFragment() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == IMPORT_BACKUP_FILE && resultCode == Activity.RESULT_OK) { - if (data != null && data.data != null) { - val uri = data.data + if (requestCode == IMPORT_BACKUP_FILE && + resultCode == Activity.RESULT_OK && + data != null && data.data != null + ) { + val uri = data.data - Log.e(TAG, "read import file: $uri") + Log.e(TAG, "read import file: $uri") - try { - val inputStream = uri?.let { - context?.contentResolver?.openInputStream(it) - } + try { + val inputStream = uri?.let { + context?.contentResolver?.openInputStream(it) + } - val bufferedReader = BufferedReader(InputStreamReader(inputStream)) - val stringBuilder = StringBuilder() + val bufferedReader = BufferedReader(InputStreamReader(inputStream)) + val stringBuilder = StringBuilder() - var line: String? - while (bufferedReader.readLine().also { line = it } != null) - stringBuilder.append(line).append('\n') + var line: String? + while (bufferedReader.readLine().also { line = it } != null) + stringBuilder.append(line).append('\n') - val type = object : TypeToken>() {}.type + val type = object : TypeToken>() {}.type - val map: Map = Gson().fromJson( - stringBuilder.toString(), - type - ) + val map: Map = Gson().fromJson( + stringBuilder.toString(), + type + ) - val editor: SharedPreferences.Editor? = - PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() + val editor: SharedPreferences.Editor? = + PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() + + for ((key, value) in map) try { + if (value is Boolean) editor?.putBoolean(key, value) + if (value is Float) editor?.putFloat(key, value) + if (value is Int) editor?.putInt(key, value) + if (value is Long) editor?.putLong(key, value) + if (value is String) editor?.putString(key, value) + if (value is Set<*>) editor?.putStringSet(key, value as Set) + } catch (e: java.lang.ClassCastException) { + e.printStackTrace() + } - for ((key, value) in map) try { - if (value is Boolean) editor?.putBoolean(key, value) - if (value is Float) editor?.putFloat(key, value) - if (value is Int) editor?.putInt(key, value) - if (value is Long) editor?.putLong(key, value) - if (value is String) editor?.putString(key, value) - if (value is Set<*>) editor?.putStringSet(key, value as Set) - } catch (e: java.lang.ClassCastException) { - e.printStackTrace() - } + editor?.apply() - editor?.apply() + Toast.makeText( + context, + getString(R.string.importing_completed), + Toast.LENGTH_SHORT + ).show() - Toast.makeText( - context, - getString(R.string.importing_completed), - Toast.LENGTH_SHORT - ).show() - - startActivity( - Intent( - context, - MainActivity::class.java - ) - ) // restart Amaze for changes to take effect - } catch (e: IOException) { - Toast.makeText( + startActivity( + Intent( context, - getString(R.string.importing_failed), - Toast.LENGTH_SHORT - ).show() - e.printStackTrace() - } + MainActivity::class.java + ) + ) // restart Amaze for changes to take effect + } catch (e: IOException) { + Toast.makeText( + context, + getString(R.string.importing_failed), + Toast.LENGTH_SHORT + ).show() + e.printStackTrace() } + } else { + Toast.makeText(context, getString(R.string.unknown_error), Toast.LENGTH_SHORT).show() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 571b87fce0..fb273e0293 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -799,5 +799,6 @@ You only need to do this once, until the next time you select a new location for Exporting failed Importing failed Importing completed + Invalid backup file From c6a1ee905c8440b69aeb54a306afe9ac20e5bd86 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sun, 16 Oct 2022 11:59:56 +0530 Subject: [PATCH 005/384] change logs to info --- .../ui/fragments/preferencefragments/BackupPrefsFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index 8b9a0f09e5..3028ed940e 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -59,7 +59,7 @@ class BackupPrefsFragment : BasePrefsFragment() { fileWriter.append(gsonString) - Log.e(TAG, "wrote export to: ${file.absolutePath}") + Log.i(TAG, "wrote export to: ${file.absolutePath}") fileWriter.flush() fileWriter.close() @@ -122,7 +122,7 @@ class BackupPrefsFragment : BasePrefsFragment() { ) { val uri = data.data - Log.e(TAG, "read import file: $uri") + Log.i(TAG, "read import file: $uri") try { val inputStream = uri?.let { From 4d52e520f7f9ed24efdc52cd3395754c05b3d593 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sun, 16 Oct 2022 12:15:01 +0530 Subject: [PATCH 006/384] use when instead of IFs --- .../preferencefragments/BackupPrefsFragment.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index 3028ed940e..0a1a358319 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -147,12 +147,14 @@ class BackupPrefsFragment : BasePrefsFragment() { PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() for ((key, value) in map) try { - if (value is Boolean) editor?.putBoolean(key, value) - if (value is Float) editor?.putFloat(key, value) - if (value is Int) editor?.putInt(key, value) - if (value is Long) editor?.putLong(key, value) - if (value is String) editor?.putString(key, value) - if (value is Set<*>) editor?.putStringSet(key, value as Set) + when (value::class.simpleName) { + "Boolean" -> editor?.putBoolean(key, value as Boolean) + "Float" -> editor?.putFloat(key, value as Float) + "Int" -> editor?.putInt(key, value as Int) + "Long" -> editor?.putLong(key, value as Long) + "String" -> editor?.putString(key, value.toString()) + "Set<*>" -> editor?.putStringSet(key, value as Set) + } } catch (e: java.lang.ClassCastException) { e.printStackTrace() } From 9e86fec8b15e30b7900997cde4c94c01b08b4298 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sun, 16 Oct 2022 12:30:59 +0530 Subject: [PATCH 007/384] updated error messages --- .../ui/fragments/preferencefragments/BackupPrefsFragment.kt | 5 +++++ app/src/main/res/values/strings.xml | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index 0a1a358319..e09df288c2 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -156,6 +156,11 @@ class BackupPrefsFragment : BasePrefsFragment() { "Set<*>" -> editor?.putStringSet(key, value as Set) } } catch (e: java.lang.ClassCastException) { + Toast.makeText( + context, + "${getString(R.string.import_failed_for)} $key", + Toast.LENGTH_SHORT + ).show() e.printStackTrace() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb273e0293..fb24ef8b08 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -796,9 +796,10 @@ You only need to do this once, until the next time you select a new location for Secure FTP Unavailable - Exporting failed - Importing failed + Exporting failed: could not write the backup file + Importing failed: could not read the backup file Importing completed Invalid backup file + Import failed for: From 5ed0463f39633661e3a712c8228d30bc58e0a44a Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 07:31:08 +0000 Subject: [PATCH 008/384] Apply translations in it translation completed for the source file '/app/src/main/res/values/strings.xml' on the 'it' language. --- app/src/main/res/values-it/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0373909391..f38bede2aa 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,4 +1,4 @@ - + @@ -105,11 +108,12 @@ Material Escuro Hora do Dia Preto (para OLED) + Sistema (seguir tema do sistema) - Pastas no início - Arquivos no início - Nada no início + Pastas primeiro + Arquivos primeiro + Nada no topo Nome Última modificação @@ -149,7 +153,7 @@ Mostra as miniaturas dos aplicativos e imagens Mostrar miniaturas Mostrar arquivos e pastas ocultos - Mostra a data e hora da última modificação do arquivo + Mostrar data e hora da última modificação Mostrar última modificação Mostra o tamanho dos arquivos e o número de itens das pastas Mostrar tamanho @@ -238,6 +242,7 @@ Escolha a pasta raíz do \t \tpara conceder acesso de gravação para operar Conexão SMB + Endereço IP do servidor Endereço IP do servidor Usuário Senha @@ -292,6 +297,7 @@ Parado Confirme se está conectado com Wi-Fi Domínio + Compartilhar caminho Inválido %s O servidor FTP está iniciando Servidor FTP @@ -424,6 +430,7 @@ Você precisará copiar este arquivo para armazenamento para abri-lo Erro: Arquivo não encontrado Erro E/S + Erro: Arquivo muito grande Texto médio Ler Gravar @@ -438,6 +445,7 @@ Conexão em nuvem Nova conexão em nuvem Dropbox + Google Drive™ OneDrive Box Obter plugin para nuvem @@ -503,7 +511,13 @@ Acessos rápidos Pasta + Favoritos da barra lateral + Mostrar favoritos Favoritos + Adicionar favorito + Editar favorito + Excluir favorito? + O plugin Amaze Cloud não foi encontrado. Faça download a partir da Play Store. Falha ao carregar a conexão. Por favor, abra o Amaze Cloud uma vez. Deseja remover a conexão com a nuvem? @@ -552,6 +566,8 @@ ATUALIZAR Conexão SCP/SFTP + Conexão FTP + FTP usando conexão SSL Porta Não é possível determinar a autenticidade do servidor \'%1$s\'.\n\n%2$s chave da impressão digital é %3$s.\n\nToque em \"Sim\" para confirmar a identidade, caso contrário, toque em \"Não\". Verificar servidor @@ -595,11 +611,22 @@ Não há informações sobre este arquivo Não foi possível, reabra a partir do último aplicativo Algo deu errado, não há nada para abrir + O arquivo é muito longo então não pode ser editado, mas você pode ler os primeiros %d caracteres O arquivo aberto é apenas para leitura. Entendi! Salvar arquivo criptografado como… Salvar pasta criptografada como… + Usar formato de criptografia Amaze + AESCrypt é um formato de criptografia de arquivo de código aberto baseado na criptografia AES. Também é portátil, pode ser descriptografado com programas AESCrypt em outras plataformas.

+

Site AESCrypt

]]> +
+ O formato de criptografia do Amaze também é baseado em criptografia AES; mas diferente do formato AESCrypt, ele só pode ser descriptografado por este dispositivo; Além disso, você não deve desinstalar o Amaze File Manager deste dispositivo nem alterar a maneira de bloquear a tela, caso contrário não terá como recuperar o arquivo criptografado.

+ ]]>
Os arquivos criptografados devem usar \".aze\" como extensão. + Arquivos criptografados devem usar \".aes\" como extensão de arquivo. + Não é possível descriptografar \'%s\': \'%s\' Não foi possível sobrescrever o arquivo. Arquivo salvo como \'%s\'. %d arquivos salvos. @@ -663,9 +690,83 @@ Escolha uma aplicação diferente Limpar cache Limpa os apps de abertura padrão selecionados + Apenas leitura Usar listagem legada para root + Se habilitado, usa método legado para listar arquivos + Arquivo RAR \"%s\" é um arquivo RAR v5 não suportado. + Você precisa reiniciar o servidor FTP para aplicar as mudanças. Permissão negada - Arquivos recentemente acessados pelo Amaze + Senha incorreta do arquivo. + Não é possível extrair \"%s\" para \"%s\". + Selecionar a preferência de arrastar e soltar (Experimental) + Pressionar e arrastar para mover ou copiar arquivos + Pressionar e arrastar para selecionar arquivos + Desabilitar + Escolha a operação para realizar + Lembrar para a próxima vez + Permitir acesso SAF para o servidor FTP + Já que essa é sua primeira vez rodando o servidor FTP, e você está usando o armazenamento interno como pasta compartilhada, por favor permita que o Amaze acesse o armazenamento por SAF.\n\n + +Você só precisa fazer isso uma vez, até a próxima vez que você selecionar um novo local para compartilhamento. + Desde o Android 10, o Android impôs restrições em certos diretórios, e eles precisam de acesso com o Storage Access Framework.\n\nPor favor permita que Amaze acesse o diretório de dentro do gerenciador de arquivos do sistema pressionando \"Permitir Acesso\". + Permitir acesso SAF + Usar sistema de arquivos legado + Habilite isso para suportar o uso de armazenamento externo de dispositivo em versões Android mais recentes + O local compartilhado do servidor FTP foi redefinido para armazenamento interno, pois você está voltando para a implementação do sistema de arquivos legado. Por favor, selecione um novo local usando o menu no canto superior direito conforme necessário. + O local compartilhado do servidor FTP foi redefinido para armazenamento interno + Você está escolhendo um local que requer acesso usuario root. Por favor habilitar o explorador root nas configurações se o seu dispositivo está rooteado. + Você está escolhendo um local que requer acesso usuario root, que habilita ler/escrever em todo o sistema de arquivos de seu dispositivo, o que é perigoso - acidentes podem \"brickar\" ou inutilizar o seu dispositivo.\n\nTem certeza? + Erro abrindo URI \"%s\" para leitura. + \"%s\" está vazio. + \"%s\" está corrompido. + \"%s\" está corrompido.\nMensagem do extrator de arquivos:\"%s\" + Arquivo não pôde ser aberto por esse aplicativo + Criar uma nova conexão Google Drive™ para gerenciar arquivos de dentro do gerenciador de arquivos Amaze + Autenticar para usar o Google Drive™ + Mostrar aplicativos do sistema + Aplicativos do sistema + Inverter seleção + Selecionar por tipo + Selecionar por data + Selecionar similares + Erro ao adquirir lista de produtos pelo Google Play. + Desde o Android 11, o Google requer que Gerenciadores de Arquivos peçam permissão para administrar todos arquivos no dispositivo. Mais detalhes aqui. + \n\nO Gerenciador de Arquivos Amaze precisa dessa permissão também. Depois de apertar "Grant" aqui, por favor selecione Permitir acesso para acessar todos arquivosna próxima tela. +\n\nCancelar esse diálogo irá fechar o aplicativo. + Aplicativos de usuários + Aparência + Comportamento + Personalizar tema, cores e aparência do gerenciador de arquivos + Controlar quais informações mostrar, personalizar a barra lateral e alterar preferência da interface. + Modificar comportamentos como pesquisa avançada e abrir arquivos como uma nova tarefa + Configurar senha e criptografia + + Arquivos acessados através do Amaze recentemente Arquivos criados ou modificados recentemente + Não foi encontrada forma de abrir arquivo + Não foi possível processar o arquivo + Analizar Armazenamento + Você está sendo redirecionado para Amaze File Utilities. É a nossa oferta que tem visualizador audio/vídeo/documentos, que permite analizar armazenamento por imagens escuras/borradas, arquivos duplicados, e transferência por Wifi P2P e mais.\nNos apoie ao experimentar. + Baixar + Wi-Fi P2P + Obrigado por instalar Amaze File Utilities. Você está sendo redirecionado para o app. + Visualizador de Imagens Amaze + Leitor de Vídeo Amaze + Leitor de Música Amaze + Visualizador de Pdf Amaze + Visualizador de Epub Amaze + Visualizador de Doc Amaze + Compartilhar logs + Compartilhar logs capturados via email / telegram + Abrir com Amaze + Confirmação + Tem certeza de que deseja abrir o seguinte arquivo?\n\nNome:\n%s\n\nLocal:\n%s\n\nTamanho:\n%s\n\nMD5:\n%s\n\nSHA256:\n%s\n\n + Pelos requerimentos da política de privacidade do Google Play, apps não são permitidos atualizarem a si mesmos. Por favor atualize o aplicativo através da Google Play. + Não foi possível abrir informações do pacote do arquivo \"%s\". Ou o arquivo especificado não é um APK, ou o arquivo está corrompido. + Excluir confirmação + Pedir confirmação antes de apagar arquivos. Desabilitar essa opção é perigoso! + FTP com segurança + Não disponível + Amaze Cloud Plugin não é compatível com a versão F-Droid. Por favor, use a versão da Play Store do Amaze. From ff511045986670af9a5226fdb6238c30138ba361 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 26 Nov 2022 12:56:45 +0530 Subject: [PATCH 013/384] fixes #3085 --- .../com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 1d2e125298..6c5660dd87 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 @@ -692,7 +692,12 @@ class SftpConnectDialog : DialogFragment() { hostKeyFingerprint: String?, encryptedPath: String ): Boolean { - DataUtils.getInstance().removeServer(DataUtils.getInstance().containsServer(oldPath)) + val i = DataUtils.getInstance().containsServer(oldPath) + + if (i != -1) { + DataUtils.getInstance().removeServer(i) + } + DataUtils.getInstance().addServer(arrayOf(connectionName, encryptedPath)) Collections.sort(DataUtils.getInstance().servers, BookSorter()) (activity as MainActivity).drawer.refreshDrawer() From 89fee6a5f67b918e5a4e3dbd71322a4b5f4db19e Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sat, 26 Nov 2022 07:33:12 +0000 Subject: [PATCH 014/384] Apply translations in it translation completed for the source file '/app/src/main/res/values/strings.xml' on the 'it' language. --- app/src/main/res/values-it/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f38bede2aa..52398e0a8e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -731,6 +731,10 @@ Dovrai farlo solo questa volta, fino a quando non sceglierai un percorso diverso Chiede una conferma prima di cancellare file. La disabilitazione di questa funzione è altamente sconsigliata! FTP sicuro Non disponibile + \"%s\n%s liberi di %s Il plugin Amaze Cloud non è supportato dalla versione F-Droid. Si prega di usare la versione di Amaze disponibile sul Play Store. + Disabilita filtri intent del player + Disabilita i filtri intent del player Amaze nel selettore dei file + Nessuna app trovata per gestire questo intent. DocumentsUI è installato? From 9f9c67f3193cb5bac790b0a7410acc2a17bcf1cf Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sun, 27 Nov 2022 13:24:06 +0000 Subject: [PATCH 015/384] Apply translations in he translation completed for the source file '/app/src/main/res/values/strings.xml' on the 'he' language. --- app/src/main/res/values-he/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 7922ffc25b..ded1e39a4e 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -779,6 +779,10 @@ לבקש אישור בטרם מחיקת קבצים. השבתת האפשרות לא מומלצת כלל! FTP מאובטח לא זמין + \"%s\n%s פנויים מתוך %s אין תמיכה בתוסף Amaze Cloud בגרסת ה־F-Droid. נא להשתמש בגרסת חנות ה־Play של Amaze. + השבתת מסנני Intent של הנגן + משבית את נגני המדיה המובנים של Amaze בבוחר הקבצים + לא נמצא יישומון לטיפול ב־Intent הזה. האם מותקן אצלך DocumentsUI? From 379fa7351e94dd5c6e835e77ddd1a0472cd0ec73 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sun, 27 Nov 2022 20:26:06 +0530 Subject: [PATCH 016/384] notify upon on-demand APK build --- .github/workflows/android-debug-artifact-ondemand.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/android-debug-artifact-ondemand.yml b/.github/workflows/android-debug-artifact-ondemand.yml index 3bcc0a19e1..03a7419914 100644 --- a/.github/workflows/android-debug-artifact-ondemand.yml +++ b/.github/workflows/android-debug-artifact-ondemand.yml @@ -11,6 +11,11 @@ jobs: runs-on: ubuntu-20.04 if: github.event.comment.body == 'Build test apk' && (github.actor == 'VishalNehra' || github.actor == 'TranceLove' || github.actor == 'EmmanuelMess' || github.actor == 'VishnuSanal') steps: + - name: Acknowledge the request with thumbs up reaction + uses: peter-evans/create-or-update-comment@v2 + with: + comment-id: ${{ github.event.comment.id }} + reactions: '+1' - name: Github API Request id: request uses: octokit/request-action@v2.0.2 @@ -49,3 +54,9 @@ jobs: with: name: Amaze-Play-debug path: app/build/outputs/apk/play/debug/app-play-debug.apk + - name: Notify the user with a comment once the APK is uploaded # TODO: update this with the link to the artifacts + uses: peter-evans/create-or-update-comment@v2 + with: + issue-number: ${{ github.event.issue.number }} + body: | + The requested APKs has been built. Please find them from the artifacts section of this PR. From bf69467c540dfd087de19b4003971a13b1f737f3 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Mon, 21 Nov 2022 02:35:40 +0530 Subject: [PATCH 017/384] Clear cloud connection credentials if loading failed after database migration Fixes #3645. After numerous attempts to fix the problem with database migration and crash on startup, 've decided to wipe out the credentials altogether. Users just need to re-authorize access to the Cloud plugin again, that should not be a problem; but on the other hand, hope this will never be the show-breaker again - this problem had exist throughout the 3.8 cycle for too long, this must be fixed. My apologies to all Amaze users again for the troubles. --- .../amaze/filemanager/database/CloudHandler.java | 4 ++++ .../filemanager/database/daos/CloudEntryDao.java | 5 +++++ .../filemanager/ui/activities/MainActivity.java | 16 ++++++++++++++-- app/src/main/res/values/strings.xml | 2 ++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java b/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java index e53636a78a..bdedf6d706 100644 --- a/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java +++ b/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java @@ -80,6 +80,10 @@ public void clear(OpenMode serviceType) { throwable -> LOG.warn("failed to delete cloud connection", throwable)); } + public void clearAllCloudConnections() { + database.cloudEntryDao().clear().subscribeOn(Schedulers.io()).blockingGet(); + } + public void updateEntry(OpenMode serviceType, CloudEntry newCloudEntry) throws CloudPluginException { diff --git a/app/src/main/java/com/amaze/filemanager/database/daos/CloudEntryDao.java b/app/src/main/java/com/amaze/filemanager/database/daos/CloudEntryDao.java index 2cd8fd767f..42e209f98c 100644 --- a/app/src/main/java/com/amaze/filemanager/database/daos/CloudEntryDao.java +++ b/app/src/main/java/com/amaze/filemanager/database/daos/CloudEntryDao.java @@ -31,6 +31,7 @@ import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; +import androidx.room.Transaction; import androidx.room.Update; import io.reactivex.Completable; @@ -62,4 +63,8 @@ public interface CloudEntryDao { @Delete Completable delete(CloudEntry entry); + + @Transaction + @Query("DELETE FROM " + TABLE_CLOUD_PERSIST) + Completable clear(); } 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 ff6c36469c..6cdce48aa8 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 @@ -359,8 +359,20 @@ public void onCreate(final Bundle savedInstanceState) { mainActivityActionMode = new MainActivityActionMode(new WeakReference<>(MainActivity.this)); if (CloudSheetFragment.isCloudProviderAvailable(this)) { - - LoaderManager.getInstance(this).initLoader(REQUEST_CODE_CLOUD_LIST_KEYS, null, this); + try { + LoaderManager.getInstance(this).initLoader(REQUEST_CODE_CLOUD_LIST_KEYS, null, this); + } catch (Exception errorRaised) { + LOG.error("Error initializing cloud connections", errorRaised); + cloudHandler.clearAllCloudConnections(); + AlertDialog.show( + this, + R.string.cloud_connection_credentials_cleared_msg, + R.string.cloud_connection_credentials_cleared, + android.R.string.ok, + null, + false); + LoaderManager.getInstance(this).initLoader(REQUEST_CODE_CLOUD_LIST_KEYS, null, this); + } } path = intent.getStringExtra("path"); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb6d37215f..63d399c5b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -794,5 +794,7 @@ You only need to do this once, until the next time you select a new location for Disable player intent filters Disables Amaze inbuilt media players from file chooser No app found to handle this intent. Do you have DocumentsUI installed? + Cloud Connection credentials cleared + Unfortunately, we were unable to migrate your cloud connection credentials to the new database schema, and we had to remove them from the app. Please create the cloud connection again. From cc2b11cc443250933de5dec8fa3e7ef8d7cc8aef Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Mon, 28 Nov 2022 22:11:03 +0800 Subject: [PATCH 018/384] Remove JUnit test report until further notice --- .github/workflows/android-feature.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/android-feature.yml b/.github/workflows/android-feature.yml index 271679d775..c661c27df0 100644 --- a/.github/workflows/android-feature.yml +++ b/.github/workflows/android-feature.yml @@ -44,11 +44,4 @@ jobs: - name: Run test cases uses: gradle/gradle-build-action@v2 with: - arguments: jacocoTestPlayDebugUnitTestReport --stacktrace --info - - name: Publish test results - uses: dorny/test-reporter@v1 - if: always() - with: - name: test-results - path: 'app/build/test-results/testPlayDebugUnitTest/TEST-*.xml' - reporter: java-junit \ No newline at end of file + arguments: jacocoTestPlayDebugUnitTestReport --stacktrace --info \ No newline at end of file From 3a78d85df4dc21ea6cef93fc5ec3c7402f947efa Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Mon, 28 Nov 2022 23:29:03 +0800 Subject: [PATCH 019/384] Fix wrong context used at GenericDialogCreation.showPasswordDialog() at ExtractService Fixes #3649 --- .../amaze/filemanager/asynchronous/services/ExtractService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java index db5b681bac..48a0d9fff9 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java @@ -388,7 +388,7 @@ protected void onProgressUpdate(IOException... values) { IOException result = values[0]; ArchivePasswordCache.getInstance().remove(compressedPath); GeneralDialogCreation.showPasswordDialog( - getApplicationContext(), + AppConfig.getInstance().getMainActivityContext(), (MainActivity) AppConfig.getInstance().getMainActivityContext(), AppConfig.getInstance().getUtilsProvider().getAppTheme(), R.string.archive_password_prompt, From 8b3d0611e986073f5cb791314de8a8012bc72ab0 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:14:14 +0000 Subject: [PATCH 020/384] Apply translations in de translation completed for the source file '/app/src/main/res/values/strings.xml' on the 'de' language. --- app/src/main/res/values-de/strings.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index be9b9d62f2..501cfc2caa 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -106,7 +106,7 @@ Hell Dunkel - Tageslicht + Gemäß Tageszeit Schwarz (für OLED) System (folgt dem System) @@ -729,6 +729,10 @@ Sie müssen diesen Schritt nur einmalig ausführen, bis Sie einen neuen Ort für Sicherheitsabfrage vor dem Löschen von Dateien. Dies sollte möglichst nicht deaktiviert werden. Secure FTP Nicht verfügbar + \"%s\n%s von %s frei Das Amaze Cloud-Plug-In wird von der F-Droid-Version nicht unterstützt. Bitte verwenden Sie die Play Store-Version von Amaze. + Aktionsfilter für Player deaktivieren + In Amaze integrierten Mediaplayer für Dateiauswahl deaktivieren + Keine App für diese Aktion gefunden. Haben Sie DocumentsUI installiert? From c1e7145340856a5fb8d40c6ef231215643abff2f Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Thu, 8 Dec 2022 05:50:58 +0530 Subject: [PATCH 021/384] Bump version to 3.8.5 --- app/build.gradle | 4 ++-- app/src/main/res/values/translators.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 92a6affb3a..efd4c4b50c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "com.amaze.filemanager" minSdkVersion 14 targetSdkVersion 31 - versionCode 117 - versionName "3.8.4" + versionCode 118 + versionName "3.8.5" multiDexEnabled true vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/res/values/translators.xml b/app/src/main/res/values/translators.xml index cadb16f0c9..cf70af665d 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.8.4 + v3.8.5 Arpit Khurana Vishal Nehra Emmanuel Messulam From fdf0039b0200ff1bacea8c9cf8fdfbb0c874e75f Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 12 Dec 2022 22:03:19 +0530 Subject: [PATCH 022/384] ReTrigger GitHub CI From 972dd2ae6cf79484623326717e03908d60bdbb7d Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 15 Dec 2022 23:14:04 +0530 Subject: [PATCH 023/384] [WIP] tests for backup and export --- .../ui/fragments/BackupPrefsFragmentTest.kt | 63 +++++++++++++++++++ .../BackupPrefsFragment.kt | 26 ++++---- 2 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt diff --git a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt new file mode 100644 index 0000000000..e02e720d4f --- /dev/null +++ b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014-2022 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.ui.fragments + +import android.content.Context +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.ui.activities.PreferencesActivity +import com.amaze.filemanager.ui.fragments.preferencefragments.BackupPrefsFragment +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File + +@RunWith(AndroidJUnit4::class) +class BackupPrefsFragmentTest { + + @Test + fun testExport() { + val backupPrefsFragment = BackupPrefsFragment() + + val activityScenario = ActivityScenario.launch(PreferencesActivity::class.java) + + activityScenario.moveToState(Lifecycle.State.STARTED) + + activityScenario.onActivity { + it.supportFragmentManager.beginTransaction() + .add(backupPrefsFragment, null) + .commitNow() + + backupPrefsFragment.exportPrefs() + } + + val file = + File( + ApplicationProvider.getApplicationContext().cacheDir.absolutePath + + File.separator + + "amaze_backup.json" + ) + + Assert.assertTrue(file.exists()) + } +} diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index e09df288c2..a3d6ee32d5 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -45,14 +45,13 @@ class BackupPrefsFragment : BasePrefsFragment() { override val title = R.string.backup - private val onExportPrefClick = OnPreferenceClickListener { - - val map: Map = PreferenceManager.getDefaultSharedPreferences(getActivity()).all + fun exportPrefs() { + val map: Map = PreferenceManager + .getDefaultSharedPreferences(requireActivity()).all val gsonString: String = Gson().toJson(map) try { - val file = File(context?.cacheDir?.absolutePath + File.separator + "amaze_backup.json") val fileWriter = FileWriter(file) @@ -82,12 +81,9 @@ class BackupPrefsFragment : BasePrefsFragment() { Toast.makeText(context, getString(R.string.exporting_failed), Toast.LENGTH_SHORT).show() e.printStackTrace() } - - true } - private val onImportPrefClick = OnPreferenceClickListener { - + fun importPrefs() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { startActivityForResult( Intent(Intent.ACTION_OPEN_DOCUMENT) @@ -109,8 +105,6 @@ class BackupPrefsFragment : BasePrefsFragment() { IMPORT_BACKUP_FILE ) } - - true } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -144,7 +138,7 @@ class BackupPrefsFragment : BasePrefsFragment() { ) val editor: SharedPreferences.Editor? = - PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() + PreferenceManager.getDefaultSharedPreferences(requireActivity()).edit() for ((key, value) in map) try { when (value::class.simpleName) { @@ -196,10 +190,16 @@ class BackupPrefsFragment : BasePrefsFragment() { findPreference( PreferencesConstants.PREFERENCE_EXPORT_SETTINGS - )?.onPreferenceClickListener = onExportPrefClick + )?.onPreferenceClickListener = OnPreferenceClickListener { + exportPrefs() + true + } findPreference( PreferencesConstants.PREFERENCE_IMPORT_SETTINGS - )?.onPreferenceClickListener = onImportPrefClick + )?.onPreferenceClickListener = OnPreferenceClickListener { + importPrefs() + true + } } } From 44ad4544877dc871da1c0fa1d1661d68180a155b Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sun, 18 Dec 2022 19:26:09 +0530 Subject: [PATCH 024/384] added tests for backup and export --- .../ui/fragments/BackupPrefsFragmentTest.kt | 172 +++++++++++++++++- .../androidTest/resources/amaze_backup.json | 33 ++++ .../BackupPrefsFragment.kt | 5 +- 3 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 app/src/androidTest/resources/amaze_backup.json diff --git a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt index e02e720d4f..11e0062e13 100644 --- a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt +++ b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt @@ -20,13 +20,24 @@ package com.amaze.filemanager.ui.fragments +import android.app.Activity import android.content.Context +import android.content.Intent +import android.net.Uri import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.R import com.amaze.filemanager.ui.activities.PreferencesActivity import com.amaze.filemanager.ui.fragments.preferencefragments.BackupPrefsFragment +import com.google.gson.* +import com.google.gson.reflect.TypeToken import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -43,6 +54,15 @@ class BackupPrefsFragmentTest { activityScenario.moveToState(Lifecycle.State.STARTED) + val exportFile = + File( + "/storage/emulated/0" + + File.separator + + "amaze_backup.json" + ) + + exportFile.delete() // delete if already exists + activityScenario.onActivity { it.supportFragmentManager.beginTransaction() .add(backupPrefsFragment, null) @@ -51,13 +71,161 @@ class BackupPrefsFragmentTest { backupPrefsFragment.exportPrefs() } - val file = + val tempFile = File( ApplicationProvider.getApplicationContext().cacheDir.absolutePath + File.separator + "amaze_backup.json" ) - Assert.assertTrue(file.exists()) + Assert.assertTrue(tempFile.exists()) + + // terrible hack :cringe: + onView(withId(R.id.home)).perform(ViewActions.click()) + Thread.sleep(500) + + onView(withText(R.string.save)).perform(ViewActions.click()) + Thread.sleep(500) + + Assert.assertTrue(exportFile.exists()) + } + + @Test + fun verifyExportFile() { + val backupPrefsFragment = BackupPrefsFragment() + + val activityScenario = ActivityScenario.launch(PreferencesActivity::class.java) + + activityScenario.moveToState(Lifecycle.State.STARTED) + + val file = + File( + "/storage/emulated/0" + + File.separator + + "amaze_backup.json" + ) + + activityScenario.onActivity { preferencesActivity -> + preferencesActivity.supportFragmentManager.beginTransaction() + .add(backupPrefsFragment, null) + .commitNow() + + val preferences = PreferenceManager + .getDefaultSharedPreferences(preferencesActivity) + + val preferenceMap: Map = preferences.all + + val inputString = file + .inputStream() + .bufferedReader() + .use { + it.readText() + } + + val type = object : TypeToken>() {}.type + + val importMap: Map = GsonBuilder() + .create() + .fromJson( + inputString, + type + ) + + for ((key, value) in preferenceMap) { + var mapValue = importMap[key] + + if (mapValue!!::class.simpleName.equals("Double")) { + mapValue = (mapValue as Double).toInt() // since Gson parses Integer as Double + } + + Assert.assertEquals(value, mapValue) + } + } + } + + @Test + fun testImport() { + val backupPrefsFragment = BackupPrefsFragment() + + val activityScenario = ActivityScenario.launch(PreferencesActivity::class.java) + + activityScenario.moveToState(Lifecycle.State.STARTED) + + val exportFile = + File( + "/storage/emulated/0" + + File.separator + + "amaze_backup.json" + ) + + exportFile.delete() // delete if already exists + + activityScenario.onActivity { preferencesActivity -> + preferencesActivity.supportFragmentManager.beginTransaction() + .add(backupPrefsFragment, null) + .commitNow() + + javaClass.getResourceAsStream("/amaze_backup.json")?.copyTo(exportFile.outputStream()) + + backupPrefsFragment.onActivityResult( + BackupPrefsFragment.IMPORT_BACKUP_FILE, + Activity.RESULT_OK, + Intent().setData( + Uri.fromFile(exportFile) + ) + ) + + val inputString = exportFile + .inputStream() + .bufferedReader() + .use { + it.readText() + } + + val type = object : TypeToken>() {}.type + + val importMap: Map = GsonBuilder() + .create() + .fromJson( + inputString, + type + ) + + val preferences = PreferenceManager + .getDefaultSharedPreferences(preferencesActivity) + + val preferenceMap: Map = preferences.all + + for ((key, value) in preferenceMap) { + when (value!!::class.simpleName) { + "Boolean" -> Assert.assertEquals( + importMap[key] as Boolean, + preferences.getBoolean(key, false) + ) + "Float" -> Assert.assertEquals( + importMap[key] as Float, + preferences.getFloat(key, 0f) + ) + "Int" -> { + // since Gson parses Integer as Double + val toInt = (importMap[key] as Double).toInt() + + Assert.assertEquals(toInt, preferences.getInt(key, 0)) + } + "Long" -> Assert.assertEquals( + importMap[key] as Long, + preferences.getLong(key, 0L) + ) + "String" -> Assert.assertEquals( + importMap[key] as String, + preferences.getString(key, null) + ) + "Set<*>" -> Assert.assertEquals( + importMap[key] as Set<*>, + preferences.getStringSet(key, null) + ) + } + } + } } } diff --git a/app/src/androidTest/resources/amaze_backup.json b/app/src/androidTest/resources/amaze_backup.json new file mode 100644 index 0000000000..b3b3ddc890 --- /dev/null +++ b/app/src/androidTest/resources/amaze_backup.json @@ -0,0 +1,33 @@ +{ + "": 1, + "showThumbs": true, + "showHidden": true, + "documents": true, + "image/jpeg_LAST": "com.oppo.gallery3d.app.ViewGallery com.coloros.gallery3d", + "video": true, + "typeablepaths": false, + "showPermissions": false, + "texteditor_newstack": false, + "legacyListing": false, + "sidebar_quickaccess_enable": false, + "fastaccess": true, + "books_added": true, + "application/pdf_LAST": "com.google.android.apps.viewer.PdfViewerActivity com.google.android.apps.docs", + "showLastModified": true, + "acra.legacyAlreadyConvertedTo4.8.0": true, + "audio/mpeg_LAST": "phone.vishnu.dialogmusicplayer.MainActivity phone.vishnu.dialogmusicplayer", + "acra.legacyAlreadyConvertedToJson": true, + "showFileSize": true, + "sidebar_bookmarks_enable": false, + "audio": true, + "needtosethome": false, + "rootmode": false, + "recent": true, + "image": true, + "savepaths": false, + "delete_confirmation": true, + "apks": true, + "audio/mpeg_DEFAULT": "phone.vishnu.dialogmusicplayer.MainActivity phone.vishnu.dialogmusicplayer", + "acra.lastVersionNr": 117, + "goBack_checkbox": false +} \ No newline at end of file diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index a3d6ee32d5..f61f26261e 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -41,7 +41,10 @@ import java.io.* class BackupPrefsFragment : BasePrefsFragment() { private val TAG: String = TagsHelper.getTag(BasePrefsFragment::class.java) - private val IMPORT_BACKUP_FILE: Int = 2 + + companion object { + val IMPORT_BACKUP_FILE: Int = 2 + } override val title = R.string.backup From 18c42a1186d2c75d9f9578bf27e9e2c81ffff91b Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 24 Dec 2022 00:24:54 +0800 Subject: [PATCH 025/384] Guard against closed cursors feeded into CloudLoaderAsyncTask --- .../com/amaze/filemanager/ui/activities/MainActivity.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 6cdce48aa8..7bc4dd3221 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 @@ -2314,7 +2314,9 @@ public void onLoadFinished(Loader loader, final Cursor data) { * * TODO: find a fix for repeated callbacks to onLoadFinished() */ - if (cloudCursorData != null && cloudCursorData == data) return; + if ((cloudCursorData != null && cloudCursorData == data) + || data.isClosed() + || cloudCursorData.isClosed()) return; cloudCursorData = data; if (cloudLoaderAsyncTask != null From d3a80d090eb8bb568078f1dea8e11af4d8ca509d Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 26 Dec 2022 18:26:15 +0530 Subject: [PATCH 026/384] codacy: Multiple occurrences of the same string literal within a single file detected. --- .../filemanager/ui/fragments/BackupPrefsFragmentTest.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt index 11e0062e13..29ee46388c 100644 --- a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt +++ b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt @@ -46,6 +46,8 @@ import java.io.File @RunWith(AndroidJUnit4::class) class BackupPrefsFragmentTest { + var storagePath = "`/storage/emulated/0`" + @Test fun testExport() { val backupPrefsFragment = BackupPrefsFragment() @@ -56,7 +58,7 @@ class BackupPrefsFragmentTest { val exportFile = File( - "/storage/emulated/0" + + storagePath + File.separator + "amaze_backup.json" ) @@ -100,7 +102,7 @@ class BackupPrefsFragmentTest { val file = File( - "/storage/emulated/0" + + storagePath + File.separator + "amaze_backup.json" ) @@ -153,7 +155,7 @@ class BackupPrefsFragmentTest { val exportFile = File( - "/storage/emulated/0" + + storagePath + File.separator + "amaze_backup.json" ) From 255127bfdc50300cfb8efcb998a73456bbcf105c Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 26 Dec 2022 18:29:57 +0530 Subject: [PATCH 027/384] codacy: Multiple occurrences of the same string literal within a single file detected. --- .../ui/fragments/BackupPrefsFragmentTest.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt index 29ee46388c..ffc4430249 100644 --- a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt +++ b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt @@ -47,6 +47,7 @@ import java.io.File class BackupPrefsFragmentTest { var storagePath = "`/storage/emulated/0`" + var fileName = "amaze_backup.json" @Test fun testExport() { @@ -60,7 +61,7 @@ class BackupPrefsFragmentTest { File( storagePath + File.separator + - "amaze_backup.json" + fileName ) exportFile.delete() // delete if already exists @@ -77,7 +78,7 @@ class BackupPrefsFragmentTest { File( ApplicationProvider.getApplicationContext().cacheDir.absolutePath + File.separator + - "amaze_backup.json" + fileName ) Assert.assertTrue(tempFile.exists()) @@ -104,7 +105,7 @@ class BackupPrefsFragmentTest { File( storagePath + File.separator + - "amaze_backup.json" + fileName ) activityScenario.onActivity { preferencesActivity -> @@ -157,7 +158,7 @@ class BackupPrefsFragmentTest { File( storagePath + File.separator + - "amaze_backup.json" + fileName ) exportFile.delete() // delete if already exists @@ -167,7 +168,7 @@ class BackupPrefsFragmentTest { .add(backupPrefsFragment, null) .commitNow() - javaClass.getResourceAsStream("/amaze_backup.json")?.copyTo(exportFile.outputStream()) + javaClass.getResourceAsStream("/$fileName")?.copyTo(exportFile.outputStream()) backupPrefsFragment.onActivityResult( BackupPrefsFragment.IMPORT_BACKUP_FILE, From 4d56aee30535282059b999a972df1e46ed041d44 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 26 Dec 2022 18:51:39 +0530 Subject: [PATCH 028/384] codacy: The function testImport is too long (69). The maximum length is 60. --- .../ui/fragments/BackupPrefsFragmentTest.kt | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt index ffc4430249..1ed2464b69 100644 --- a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt +++ b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt @@ -23,6 +23,7 @@ package com.amaze.filemanager.ui.fragments import android.app.Activity import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.net.Uri import androidx.lifecycle.Lifecycle import androidx.preference.PreferenceManager @@ -200,35 +201,35 @@ class BackupPrefsFragmentTest { val preferenceMap: Map = preferences.all for ((key, value) in preferenceMap) { - when (value!!::class.simpleName) { - "Boolean" -> Assert.assertEquals( - importMap[key] as Boolean, - preferences.getBoolean(key, false) - ) - "Float" -> Assert.assertEquals( - importMap[key] as Float, - preferences.getFloat(key, 0f) - ) - "Int" -> { - // since Gson parses Integer as Double - val toInt = (importMap[key] as Double).toInt() - - Assert.assertEquals(toInt, preferences.getInt(key, 0)) - } - "Long" -> Assert.assertEquals( - importMap[key] as Long, - preferences.getLong(key, 0L) - ) - "String" -> Assert.assertEquals( - importMap[key] as String, - preferences.getString(key, null) - ) - "Set<*>" -> Assert.assertEquals( - importMap[key] as Set<*>, - preferences.getStringSet(key, null) - ) - } + Assert.assertTrue(checkPrefEqual(preferences, importMap, key, value)) + } + } + } + + private fun checkPrefEqual( + preferences: SharedPreferences, + importMap: Map, + key: String?, + value: Any? + ): Boolean { + when (value!!::class.simpleName) { + "Boolean" -> return importMap[key] as Boolean == + preferences.getBoolean(key, false) + "Float" -> importMap[key] as Float == + preferences.getFloat(key, 0f) + "Int" -> { + // since Gson parses Integer as Double + val toInt = (importMap[key] as Double).toInt() + + return toInt == preferences.getInt(key, 0) } + "Long" -> return importMap[key] as Long == + preferences.getLong(key, 0L) + "String" -> return importMap[key] as String == + preferences.getString(key, null) + "Set<*>" -> return importMap[key] as Set<*> == + preferences.getStringSet(key, null) } + return false } } From fdd725e403287ec62dd8a8d1ce7f84bd8131c1b5 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 26 Dec 2022 18:52:08 +0530 Subject: [PATCH 029/384] fix string :facepalm: --- .../amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt index 1ed2464b69..cd852ae6a3 100644 --- a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt +++ b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt @@ -47,7 +47,7 @@ import java.io.File @RunWith(AndroidJUnit4::class) class BackupPrefsFragmentTest { - var storagePath = "`/storage/emulated/0`" + var storagePath = "/storage/emulated/0" var fileName = "amaze_backup.json" @Test From ff89364cefecb46ea6f7ded95f9a2497e1ac25dd Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 26 Dec 2022 19:03:01 +0530 Subject: [PATCH 030/384] codacy: This condition is too complex (4). Defined complexity threshold for conditions is set to '4' --- .../ui/fragments/preferencefragments/BackupPrefsFragment.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index f61f26261e..b4aa10538e 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -113,11 +113,13 @@ class BackupPrefsFragment : BasePrefsFragment() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) + val nonNull = data != null && data.data != null + if (requestCode == IMPORT_BACKUP_FILE && resultCode == Activity.RESULT_OK && - data != null && data.data != null + nonNull ) { - val uri = data.data + val uri = data!!.data Log.i(TAG, "read import file: $uri") From c9795a72547cf7a815e8f89f3c42437ffd78b40e Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 26 Dec 2022 19:07:06 +0530 Subject: [PATCH 031/384] codacy: The function onActivityResult is too long (65). The maximum length is 60. --- .../BackupPrefsFragment.kt | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index b4aa10538e..ba63d99aae 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -145,23 +145,8 @@ class BackupPrefsFragment : BasePrefsFragment() { val editor: SharedPreferences.Editor? = PreferenceManager.getDefaultSharedPreferences(requireActivity()).edit() - for ((key, value) in map) try { - when (value::class.simpleName) { - "Boolean" -> editor?.putBoolean(key, value as Boolean) - "Float" -> editor?.putFloat(key, value as Float) - "Int" -> editor?.putInt(key, value as Int) - "Long" -> editor?.putLong(key, value as Long) - "String" -> editor?.putString(key, value.toString()) - "Set<*>" -> editor?.putStringSet(key, value as Set) - } - } catch (e: java.lang.ClassCastException) { - Toast.makeText( - context, - "${getString(R.string.import_failed_for)} $key", - Toast.LENGTH_SHORT - ).show() - e.printStackTrace() - } + for ((key, value) in map) + putBoolean(editor, key, value) editor?.apply() @@ -190,6 +175,27 @@ class BackupPrefsFragment : BasePrefsFragment() { } } + @Suppress("UNCHECKED_CAST") + private fun putBoolean(editor: SharedPreferences.Editor?, key: String?, value: Any) { + try { + when (value::class.simpleName) { + "Boolean" -> editor?.putBoolean(key, value as Boolean) + "Float" -> editor?.putFloat(key, value as Float) + "Int" -> editor?.putInt(key, value as Int) + "Long" -> editor?.putLong(key, value as Long) + "String" -> editor?.putString(key, value.toString()) + "Set<*>" -> editor?.putStringSet(key, value as Set) + } + } catch (e: java.lang.ClassCastException) { + Toast.makeText( + context, + "${getString(R.string.import_failed_for)} $key", + Toast.LENGTH_SHORT + ).show() + e.printStackTrace() + } + } + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.backup_prefs, rootKey) From 77d5c405b1ff3e10e3e1c45a61293ee6f059d94e Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 26 Dec 2022 19:18:11 +0530 Subject: [PATCH 032/384] codacy: The function is missing documentation. --- .../amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt | 3 +++ .../ui/fragments/preferencefragments/BackupPrefsFragment.kt | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt index cd852ae6a3..a11ebb0fad 100644 --- a/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt +++ b/app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt @@ -50,6 +50,7 @@ class BackupPrefsFragmentTest { var storagePath = "/storage/emulated/0" var fileName = "amaze_backup.json" + /** Test exporting */ @Test fun testExport() { val backupPrefsFragment = BackupPrefsFragment() @@ -94,6 +95,7 @@ class BackupPrefsFragmentTest { Assert.assertTrue(exportFile.exists()) } + /** Test whether the exported file contains the expected preference values */ @Test fun verifyExportFile() { val backupPrefsFragment = BackupPrefsFragment() @@ -147,6 +149,7 @@ class BackupPrefsFragmentTest { } } + /** Test import */ @Test fun testImport() { val backupPrefsFragment = BackupPrefsFragment() diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index ba63d99aae..9f74d9a963 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -48,6 +48,7 @@ class BackupPrefsFragment : BasePrefsFragment() { override val title = R.string.backup + /** Export app settings to a JSON file */ fun exportPrefs() { val map: Map = PreferenceManager .getDefaultSharedPreferences(requireActivity()).all @@ -86,7 +87,8 @@ class BackupPrefsFragment : BasePrefsFragment() { } } - fun importPrefs() { + /** Import app settings from a JSON file */ + private fun importPrefs() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { startActivityForResult( Intent(Intent.ACTION_OPEN_DOCUMENT) From a946982c206b58f27cfd1f44dc63a81b86e6046a Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 29 Dec 2022 12:51:29 +0530 Subject: [PATCH 033/384] fixes #3673 --- .../asynchronous/asynctasks/LoadFilesListTask.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 910216624c..4cdb1efd44 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 @@ -264,7 +264,14 @@ private void postListCustomPathProcess( MainFragmentViewModel viewModel = mainFragment.getMainFragmentViewModel(); - for (LayoutElementParcelable layoutElementParcelable : list) { + for (int i = 0; i < list.size(); i++) { + LayoutElementParcelable layoutElementParcelable = list.get(i); + + if (layoutElementParcelable == null) { + list.remove(i); + continue; + } + if (layoutElementParcelable.isDirectory) { viewModel.setFolderCount(mainFragment.getMainFragmentViewModel().getFolderCount() + 1); } else { From 77111f0f6c7b92bbf9cdc6fed18403cf2a59c1b8 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 29 Dec 2022 13:32:52 +0530 Subject: [PATCH 034/384] fixes #3666 since there was ```if (!hFile.getPath().endsWith("/")) { hFile.setPath(hFile.getPath() + "/"); }``` before -- please CMIIW --- .../filemanager/asynchronous/asynctasks/LoadFilesListTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4cdb1efd44..f22e03b570 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 @@ -594,7 +594,7 @@ private List listSmb( _file = new HybridFile(OpenMode.SMB, path); } if (!_file.getPath().endsWith("/")) { - _file.setPath(hFile.getPath() + "/"); + _file.setPath(_file.getPath() + "/"); } @NonNull List list; List smbCache = mainActivityViewModel.getFromListCache(path); From 71626cc10ede49017f39f197808c5c1d3fb18e9b Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 29 Dec 2022 15:08:42 +0530 Subject: [PATCH 035/384] fixes #3638 --- .../amaze/filemanager/filesystem/HybridFile.java | 4 ++++ .../filemanager/ui/fragments/MainFragment.java | 13 ++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) 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 fe3bbb020c..39823e6391 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -533,6 +533,7 @@ public boolean isCustomPath() { } /** Helper method to get parent path */ + @Nullable public String getParent(Context context) { switch (mode) { case SMB: @@ -554,6 +555,9 @@ public String getParent(Context context) { } } List pathSegments = Uri.parse(thisPath).getPathSegments(); + + if (thisPath.isEmpty() || pathSegments.isEmpty()) return null; + String currentName = pathSegments.get(pathSegments.size() - 1); String parent = thisPath.substring(0, thisPath.lastIndexOf(currentName)); if (ArraysKt.any(ANDROID_DATA_DIRS, dir -> parent.endsWith(dir + "/"))) { 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 a04ae74723..6b4963b44a 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 @@ -1050,11 +1050,14 @@ public void goBack() { } else if (OpenMode.DOCUMENT_FILE.equals(mainFragmentViewModel.getOpenMode())) { loadlist(currentFile.getParent(getContext()), true, currentFile.getMode(), false); } else { - loadlist( - currentFile.getParent(getContext()), - true, - mainFragmentViewModel.getOpenMode(), - false); + + String parent = currentFile.getParent(getContext()); + + if (parent == null) + parent = + mainFragmentViewModel.getHome(); // fall back by traversing back to home folder + + loadlist(parent, true, mainFragmentViewModel.getOpenMode(), false); } } else if (OpenMode.FTP.equals(mainFragmentViewModel.getOpenMode())) { if (mainFragmentViewModel.getCurrentPath() != null) { From 75015c8e4353771ece5ff49dc6cb8e52fc259453 Mon Sep 17 00:00:00 2001 From: TranceLove Date: Thu, 29 Dec 2022 15:11:26 +0800 Subject: [PATCH 036/384] commons-compress library upgrade to 1.22 Prevent out of memory errors. Additionally allows us to specify the max memory available during processing 7z archives. --- build.gradle | 2 +- commons_compress_7z/build.gradle | 4 +- .../sevenz/AES256SHA256Decoder.java | 20 +- .../filesystem/compressed/sevenz/Archive.java | 6 +- .../filesystem/compressed/sevenz/Coder.java | 2 +- .../compressed/sevenz/CoderBase.java | 13 +- .../filesystem/compressed/sevenz/Coders.java | 29 +- .../compressed/sevenz/DeltaDecoder.java | 5 +- .../filesystem/compressed/sevenz/Folder.java | 34 +- .../filesystem/compressed/sevenz/IOUtils.java | 288 ++++ .../compressed/sevenz/LZMA2Decoder.java | 24 +- .../compressed/sevenz/LZMADecoder.java | 36 +- .../compressed/sevenz/SevenZArchiveEntry.java | 83 +- .../compressed/sevenz/SevenZFile.java | 1428 ++++++++++++++--- .../compressed/sevenz/SevenZFileOptions.java | 167 ++ .../compressed/sevenz/SevenZMethod.java | 5 +- .../sevenz/SevenZMethodConfiguration.java | 22 +- .../compressed/sevenz/SevenZOutputFile.java | 42 +- .../filesystem/compressed/sevenz/package.html | 6 +- 19 files changed, 1926 insertions(+), 290 deletions(-) create mode 100644 commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/IOUtils.java create mode 100644 commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZFileOptions.java diff --git a/build.gradle b/build.gradle index 89dcd9c767..94f25bf6b9 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ buildscript { espressoVersion = "3.4.0" materialDialogsVersion = "0.9.6.0" jacocoVersion = "0.8.7" - commonsCompressVersion = "1.20" + commonsCompressVersion = "1.22" libsuVersion = "3.2.1" mockkVersion = "1.12.2" logbackAndroidVersion = "2.0.0" diff --git a/commons_compress_7z/build.gradle b/commons_compress_7z/build.gradle index fcb4ccca7e..a550f9a908 100644 --- a/commons_compress_7z/build.gradle +++ b/commons_compress_7z/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { compileSdkVersion 28 @@ -21,5 +22,6 @@ android { dependencies { implementation "org.apache.commons:commons-compress:$commonsCompressVersion" - implementation 'org.tukaani:xz:1.8' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.tukaani:xz:1.9' } diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/AES256SHA256Decoder.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/AES256SHA256Decoder.java index 4961b194d5..146e672bfd 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/AES256SHA256Decoder.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/AES256SHA256Decoder.java @@ -41,16 +41,22 @@ InputStream decode( final InputStream in, final long uncompressedLength, final Coder coder, - final byte[] passwordBytes) - throws IOException { + final byte[] passwordBytes, + final int maxMemoryLimitInKb) { return new InputStream() { - private boolean isInitialized = false; - private CipherInputStream cipherInputStream = null; + private boolean isInitialized; + private CipherInputStream cipherInputStream; private CipherInputStream init() throws IOException { if (isInitialized) { return cipherInputStream; } + if (coder.properties == null) { + throw new IOException("Missing AES256 properties in " + archiveName); + } + if (coder.properties.length < 2) { + throw new IOException("AES256 properties too short in " + archiveName); + } final int byte0 = 0xff & coder.properties[0]; final int numCyclesPower = byte0 & 0x3f; final int byte1 = 0xff & coder.properties[1]; @@ -126,7 +132,11 @@ public int read(final byte[] b, final int off, final int len) throws IOException } @Override - public void close() {} + public void close() throws IOException { + if (cipherInputStream != null) { + cipherInputStream.close(); + } + } }; } } diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Archive.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Archive.java index b2544a53aa..945e768a74 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Archive.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Archive.java @@ -26,17 +26,17 @@ class Archive { /// Offset from beginning of file + SIGNATURE_HEADER_SIZE to packed streams. long packPos; /// Size of each packed stream. - long[] packSizes; + long[] packSizes = new long[0]; /// Whether each particular packed streams has a CRC. BitSet packCrcsDefined; /// CRCs for each packed stream, valid only if that packed stream has one. long[] packCrcs; /// Properties of solid compression blocks. - Folder[] folders; + Folder[] folders = Folder.EMPTY_FOLDER_ARRAY; /// Temporary properties for non-empty files (subsumed into the files array later). SubStreamsInfo subStreamsInfo; /// The files and directories in the archive. - SevenZArchiveEntry[] files; + SevenZArchiveEntry[] files = SevenZArchiveEntry.EMPTY_SEVEN_Z_ARCHIVE_ENTRY_ARRAY; /// Mapping between folders, files and streams. StreamMap streamMap; diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Coder.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Coder.java index b935c52fc3..f8e676ae00 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Coder.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Coder.java @@ -24,5 +24,5 @@ class Coder { byte[] decompressionMethodId; long numInStreams; long numOutStreams; - byte[] properties = null; + byte[] properties; } diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/CoderBase.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/CoderBase.java index 3dde55a208..f43b19565f 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/CoderBase.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/CoderBase.java @@ -24,11 +24,11 @@ import java.io.InputStream; import java.io.OutputStream; +import org.apache.commons.compress.utils.ByteUtils; + /** Base Codec class. */ abstract class CoderBase { private final Class[] acceptableOptions; - private static final byte[] NONE = new byte[0]; - /** * @param acceptableOptions types that can be used as options for this codec. */ @@ -52,7 +52,7 @@ boolean canAcceptOptions(final Object opts) { * @return property-bytes to write in a Folder block */ byte[] getOptionsAsProperties(final Object options) throws IOException { - return NONE; + return ByteUtils.EMPTY_BYTE_ARRAY; } /** @@ -69,16 +69,17 @@ Object getOptionsFromCoder(final Coder coder, final InputStream in) throws IOExc abstract InputStream decode( final String archiveName, final InputStream in, - long uncomressedLength, + long uncompressedLength, final Coder coder, - byte[] password) + byte[] password, + int maxMemoryLimitInKb) throws IOException; /** * @return a stream that writes to out using the given configuration. */ OutputStream encode(final OutputStream out, final Object options) throws IOException { - throw new UnsupportedOperationException("method doesn't support writing"); + throw new UnsupportedOperationException("Method doesn't support writing"); } /** diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Coders.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Coders.java index 31cd9eb4f6..f616a0bfa1 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Coders.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Coders.java @@ -79,7 +79,8 @@ static InputStream addDecoder( final InputStream is, final long uncompressedLength, final Coder coder, - final byte[] password) + final byte[] password, + final int maxMemoryLimitInKb) throws IOException { final CoderBase cb = findByMethod(SevenZMethod.byId(coder.decompressionMethodId)); if (cb == null) { @@ -89,7 +90,7 @@ static InputStream addDecoder( + " used in " + archiveName); } - return cb.decode(archiveName, is, uncompressedLength, coder, password); + return cb.decode(archiveName, is, uncompressedLength, coder, password, maxMemoryLimitInKb); } static OutputStream addEncoder( @@ -108,7 +109,8 @@ InputStream decode( final InputStream in, final long uncompressedLength, final Coder coder, - final byte[] password) + final byte[] password, + final int maxMemoryLimitInKb) throws IOException { return in; } @@ -132,7 +134,8 @@ InputStream decode( final InputStream in, final long uncompressedLength, final Coder coder, - final byte[] password) + final byte[] password, + final int maxMemoryLimitInKb) throws IOException { try { return opts.getInputStream(in); @@ -168,7 +171,8 @@ InputStream decode( final InputStream in, final long uncompressedLength, final Coder coder, - final byte[] password) + final byte[] password, + final int maxMemoryLimitInKb) throws IOException { final Inflater inflater = new Inflater(true); // Inflater with nowrap=true has this odd contract for a zero padding @@ -192,10 +196,11 @@ OutputStream encode(final OutputStream out, final Object options) { static class DeflateDecoderInputStream extends InputStream { - InflaterInputStream inflaterInputStream; + final InflaterInputStream inflaterInputStream; Inflater inflater; - public DeflateDecoderInputStream(InflaterInputStream inflaterInputStream, Inflater inflater) { + public DeflateDecoderInputStream( + final InflaterInputStream inflaterInputStream, final Inflater inflater) { this.inflaterInputStream = inflaterInputStream; this.inflater = inflater; } @@ -227,11 +232,11 @@ public void close() throws IOException { static class DeflateDecoderOutputStream extends OutputStream { - DeflaterOutputStream deflaterOutputStream; + final DeflaterOutputStream deflaterOutputStream; Deflater deflater; public DeflateDecoderOutputStream( - DeflaterOutputStream deflaterOutputStream, Deflater deflater) { + final DeflaterOutputStream deflaterOutputStream, final Deflater deflater) { this.deflaterOutputStream = deflaterOutputStream; this.deflater = deflater; } @@ -274,7 +279,8 @@ InputStream decode( final InputStream in, final long uncompressedLength, final Coder coder, - final byte[] password) + final byte[] password, + final int maxMemoryLimitInKb) throws IOException { return new Deflate64CompressorInputStream(in); } @@ -291,7 +297,8 @@ InputStream decode( final InputStream in, final long uncompressedLength, final Coder coder, - final byte[] password) + final byte[] password, + final int maxMemoryLimitInKb) throws IOException { return new BZip2CompressorInputStream(in); } diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/DeltaDecoder.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/DeltaDecoder.java index 92bcefa347..867e57f443 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/DeltaDecoder.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/DeltaDecoder.java @@ -39,7 +39,8 @@ InputStream decode( final InputStream in, final long uncompressedLength, final Coder coder, - final byte[] password) + final byte[] password, + final int maxMemoryLimitInKb) throws IOException { return new DeltaOptions(getOptionsFromCoder(coder)).getInputStream(in); } @@ -50,7 +51,7 @@ OutputStream encode(final OutputStream out, final Object options) throws IOExcep final int distance = numberOptionOrDefault(options, 1); try { return new DeltaOptions(distance).getOutputStream(new FinishableWrapperOutputStream(out)); - } catch (final UnsupportedOptionsException ex) { + } catch (final UnsupportedOptionsException ex) { // NOSONAR throw new IOException(ex.getMessage()); } } diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Folder.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Folder.java index 75eba8374d..2f18f371e4 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Folder.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/Folder.java @@ -20,6 +20,8 @@ package com.amaze.filemanager.filesystem.compressed.sevenz; +import java.io.IOException; +import java.util.Collections; import java.util.LinkedList; /** The unit of solid compression. */ @@ -42,9 +44,10 @@ class Folder { /// The CRC, if present. long crc; /// The number of unpack substreams, product of the number of - /// output streams and the nuber of non-empty files in this + /// output streams and the number of non-empty files in this /// folder. int numUnpackSubStreams; + static final Folder[] EMPTY_FOLDER_ARRAY = new Folder[0]; /** * Sorts Coders using bind pairs. @@ -52,10 +55,19 @@ class Folder { *

The first coder reads from the packed stream (we currently only support single input stream * decoders), the second reads from the output of the first and so on. */ - Iterable getOrderedCoders() { + Iterable getOrderedCoders() throws IOException { + if (packedStreams == null + || coders == null + || packedStreams.length == 0 + || coders.length == 0) { + return Collections.emptyList(); + } final LinkedList l = new LinkedList<>(); int current = (int) packedStreams[0]; // more that 2^31 coders? - while (current != -1) { + while (current >= 0 && current < coders.length) { + if (l.contains(coders[current])) { + throw new IOException("folder uses the same coder more than once in coder chain"); + } l.addLast(coders[current]); final int pair = findBindPairForOutStream(current); current = pair != -1 ? (int) bindPairs[pair].inIndex : -1; @@ -64,18 +76,22 @@ Iterable getOrderedCoders() { } int findBindPairForInStream(final int index) { - for (int i = 0; i < bindPairs.length; i++) { - if (bindPairs[i].inIndex == index) { - return i; + if (bindPairs != null) { + for (int i = 0; i < bindPairs.length; i++) { + if (bindPairs[i].inIndex == index) { + return i; + } } } return -1; } int findBindPairForOutStream(final int index) { - for (int i = 0; i < bindPairs.length; i++) { - if (bindPairs[i].outIndex == index) { - return i; + if (bindPairs != null) { + for (int i = 0; i < bindPairs.length; i++) { + if (bindPairs[i].outIndex == index) { + return i; + } } } return -1; diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/IOUtils.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/IOUtils.java new file mode 100644 index 0000000000..e576100271 --- /dev/null +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/IOUtils.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2014-2022 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.filesystem.compressed.sevenz; + +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; + +import kotlin.io.ByteStreamsKt; + +/** + * A trimmed down version of original {@link org.apache.commons.compress.utils.IOUtils} but without + * the existence of classes that won't exist in older Androids, as well as removing methods that can + * be removed. + */ +class IOUtils { + + private static final int COPY_BUF_SIZE = 8024; + private static final int SKIP_BUF_SIZE = 4096; + + // This buffer does not need to be synchronized because it is write only; the contents are ignored + // Does not affect Immutability + private static final byte[] SKIP_BUF = new byte[SKIP_BUF_SIZE]; + + /** Private constructor to prevent instantiation of this utility class. */ + private IOUtils() {} + + /** + * Copies the content of a InputStream into an OutputStream. Uses a default buffer size of 8024 + * bytes. + * + * @param input the InputStream to copy + * @param output the target, may be null to simulate output to dev/null on Linux and NUL on + * Windows + * @return the number of bytes copied + * @throws IOException if an error occurs + */ + public static long copy(final InputStream input, final OutputStream output) throws IOException { + return copy(input, output, COPY_BUF_SIZE); + } + + /** + * Copies the content of a InputStream into an OutputStream + * + * @param input the InputStream to copy + * @param output the target, may be null to simulate output to dev/null on Linux and NUL on + * Windows + * @param buffersize the buffer size to use, must be bigger than 0 + * @return the number of bytes copied + * @throws IOException if an error occurs + * @throws IllegalArgumentException if buffersize is smaller than or equal to 0 + */ + public static long copy(final InputStream input, final OutputStream output, final int buffersize) + throws IOException { + return ByteStreamsKt.copyTo(input, output, buffersize); + } + + /** + * Skips the given number of bytes by repeatedly invoking skip on the given input stream if + * necessary. + * + *

In a case where the stream's skip() method returns 0 before the requested number of bytes + * has been skip this implementation will fall back to using the read() method. + * + *

This method will only skip less than the requested number of bytes if the end of the input + * stream has been reached. + * + * @param input stream to skip bytes in + * @param numToSkip the number of bytes to skip + * @return the number of bytes actually skipped + * @throws IOException on error + */ + public static long skip(final InputStream input, long numToSkip) throws IOException { + final long available = numToSkip; + while (numToSkip > 0) { + final long skipped = input.skip(numToSkip); + if (skipped == 0) { + break; + } + numToSkip -= skipped; + } + + while (numToSkip > 0) { + final int read = readFully(input, SKIP_BUF, 0, (int) Math.min(numToSkip, SKIP_BUF_SIZE)); + if (read < 1) { + break; + } + numToSkip -= read; + } + return available - numToSkip; + } + + /** + * Reads as much from input as possible to fill the given array with the given amount of bytes. + * + *

This method may invoke read repeatedly to read the bytes and only read less bytes than the + * requested length if the end of the stream has been reached. + * + * @param input stream to read from + * @param array buffer to fill + * @param offset offset into the buffer to start filling at + * @param len of bytes to read + * @return the number of bytes actually read + * @throws IOException if an I/O error has occurred + */ + public static int readFully( + final InputStream input, final byte[] array, final int offset, final int len) + throws IOException { + if (len < 0 || offset < 0 || len + offset > array.length || len + offset < 0) { + throw new IndexOutOfBoundsException(); + } + int count = 0, x = 0; + while (count != len) { + x = input.read(array, offset + count, len - count); + if (x == -1) { + break; + } + count += x; + } + return count; + } + + /** + * Reads {@code b.remaining()} bytes from the given channel starting at the current channel's + * position. + * + *

This method reads repeatedly from the channel until the requested number of bytes are read. + * This method blocks until the requested number of bytes are read, the end of the channel is + * detected, or an exception is thrown. + * + * @param channel the channel to read from + * @param byteBuffer the buffer into which the data is read. + * @throws IOException - if an I/O error occurs. + * @throws EOFException - if the channel reaches the end before reading all the bytes. + */ + public static void readFully(final ReadableByteChannel channel, final ByteBuffer byteBuffer) + throws IOException { + final int expectedLength = byteBuffer.remaining(); + int read = 0; + while (read < expectedLength) { + final int readNow = channel.read(byteBuffer); + if (readNow <= 0) { + break; + } + read += readNow; + } + if (read < expectedLength) { + throw new EOFException(); + } + } + + // toByteArray(InputStream) copied from: + // commons/proper/io/trunk/src/main/java/org/apache/commons/io/IOUtils.java?revision=1428941 + // January 8th, 2013 + // + // Assuming our copy() works just as well as theirs! :-) + + /** + * Gets the contents of an {@code InputStream} as a {@code byte[]}. + * + *

This method buffers the input internally, so there is no need to use a {@code + * BufferedInputStream}. + * + * @param input the {@code InputStream} to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.5 + */ + public static byte[] toByteArray(final InputStream input) throws IOException { + return ByteStreamsKt.readBytes(input); + } + + /** + * Copies part of the content of a InputStream into an OutputStream. Uses a default buffer size of + * 8024 bytes. + * + * @param input the InputStream to copy + * @param output the target Stream + * @param len maximum amount of bytes to copy + * @return the number of bytes copied + * @throws IOException if an error occurs + * @since 1.21 + */ + public static long copyRange(final InputStream input, final long len, final OutputStream output) + throws IOException { + return copyRange(input, len, output, COPY_BUF_SIZE); + } + + /** + * Copies part of the content of a InputStream into an OutputStream + * + * @param input the InputStream to copy + * @param len maximum amount of bytes to copy + * @param output the target, may be null to simulate output to dev/null on Linux and NUL on + * Windows + * @param buffersize the buffer size to use, must be bigger than 0 + * @return the number of bytes copied + * @throws IOException if an error occurs + * @throws IllegalArgumentException if buffersize is smaller than or equal to 0 + * @since 1.21 + */ + public static long copyRange( + final InputStream input, final long len, final OutputStream output, final int buffersize) + throws IOException { + if (buffersize < 1) { + throw new IllegalArgumentException("buffersize must be bigger than 0"); + } + final byte[] buffer = new byte[(int) Math.min(buffersize, len)]; + int n = 0; + long count = 0; + while (count < len + && -1 != (n = input.read(buffer, 0, (int) Math.min(len - count, buffer.length)))) { + if (output != null) { + output.write(buffer, 0, n); + } + count += n; + } + return count; + } + + /** + * Gets part of the contents of an {@code InputStream} as a {@code byte[]}. + * + * @param input the {@code InputStream} to read from + * @param len maximum amount of bytes to copy + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.21 + */ + public static byte[] readRange(final InputStream input, final int len) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + copyRange(input, len, output); + return output.toByteArray(); + } + + /** + * Gets part of the contents of an {@code ReadableByteChannel} as a {@code byte[]}. + * + * @param input the {@code ReadableByteChannel} to read from + * @param len maximum amount of bytes to copy + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.21 + */ + public static byte[] readRange(final ReadableByteChannel input, final int len) + throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final ByteBuffer b = ByteBuffer.allocate(Math.min(len, COPY_BUF_SIZE)); + int read = 0; + while (read < len) { + // Make sure we never read more than len bytes + b.limit(Math.min(len - read, b.capacity())); + final int readNow = input.read(b); + if (readNow <= 0) { + break; + } + output.write(b.array(), 0, readNow); + b.rewind(); + read += readNow; + } + return output.toByteArray(); + } +} diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/LZMA2Decoder.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/LZMA2Decoder.java index 582b2b5d50..ce8cc422ed 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/LZMA2Decoder.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/LZMA2Decoder.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.io.OutputStream; +import org.apache.commons.compress.MemoryLimitException; import org.tukaani.xz.FinishableOutputStream; import org.tukaani.xz.FinishableWrapperOutputStream; import org.tukaani.xz.LZMA2InputStream; @@ -40,12 +41,17 @@ InputStream decode( final InputStream in, final long uncompressedLength, final Coder coder, - final byte[] password) + final byte[] password, + final int maxMemoryLimitInKb) throws IOException { try { final int dictionarySize = getDictionarySize(coder); + final int memoryUsageInKb = LZMA2InputStream.getMemoryUsage(dictionarySize); + if (memoryUsageInKb > maxMemoryLimitInKb) { + throw new MemoryLimitException(memoryUsageInKb, maxMemoryLimitInKb); + } return new LZMA2InputStream(in, dictionarySize); - } catch (final IllegalArgumentException ex) { + } catch (final IllegalArgumentException ex) { // NOSONAR throw new IOException(ex.getMessage()); } } @@ -66,7 +72,7 @@ byte[] getOptionsAsProperties(final Object opts) { } @Override - Object getOptionsFromCoder(final Coder coder, final InputStream in) { + Object getOptionsFromCoder(final Coder coder, final InputStream in) throws IOException { return getDictionarySize(coder); } @@ -77,13 +83,19 @@ private int getDictSize(final Object opts) { return numberOptionOrDefault(opts); } - private int getDictionarySize(final Coder coder) throws IllegalArgumentException { + private int getDictionarySize(final Coder coder) throws IOException { + if (coder.properties == null) { + throw new IOException("Missing LZMA2 properties"); + } + if (coder.properties.length < 1) { + throw new IOException("LZMA2 properties too short"); + } final int dictionarySizeBits = 0xff & coder.properties[0]; if ((dictionarySizeBits & (~0x3f)) != 0) { - throw new IllegalArgumentException("Unsupported LZMA2 property bits"); + throw new IOException("Unsupported LZMA2 property bits"); } if (dictionarySizeBits > 40) { - throw new IllegalArgumentException("Dictionary larger than 4GiB maximum size"); + throw new IOException("Dictionary larger than 4GiB maximum size"); } if (dictionarySizeBits == 40) { return 0xFFFFffff; diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/LZMADecoder.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/LZMADecoder.java index 3d14e20e9d..6a2924bed1 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/LZMADecoder.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/LZMADecoder.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.io.OutputStream; +import org.apache.commons.compress.MemoryLimitException; import org.apache.commons.compress.utils.ByteUtils; import org.apache.commons.compress.utils.FlushShieldFilterOutputStream; import org.tukaani.xz.LZMA2Options; @@ -41,14 +42,27 @@ InputStream decode( final InputStream in, final long uncompressedLength, final Coder coder, - final byte[] password) + final byte[] password, + final int maxMemoryLimitInKb) throws IOException { + if (coder.properties == null) { + throw new IOException("Missing LZMA properties"); + } + if (coder.properties.length < 1) { + throw new IOException("LZMA properties too short"); + } final byte propsByte = coder.properties[0]; final int dictSize = getDictionarySize(coder); if (dictSize > LZMAInputStream.DICT_SIZE_MAX) { throw new IOException("Dictionary larger than 4GiB maximum size used in " + archiveName); } - return new LZMAInputStream(in, uncompressedLength, propsByte, dictSize); + final int memoryUsageInKb = LZMAInputStream.getMemoryUsage(dictSize, propsByte); + if (memoryUsageInKb > maxMemoryLimitInKb) { + throw new MemoryLimitException(memoryUsageInKb, maxMemoryLimitInKb); + } + final LZMAInputStream lzmaIn = new LZMAInputStream(in, uncompressedLength, propsByte, dictSize); + lzmaIn.enableRelaxedEndCondition(); + return lzmaIn; } @SuppressWarnings("resource") @@ -62,8 +76,8 @@ OutputStream encode(final OutputStream out, final Object opts) throws IOExceptio byte[] getOptionsAsProperties(final Object opts) throws IOException { final LZMA2Options options = getOptions(opts); final byte props = (byte) ((options.getPb() * 5 + options.getLp()) * 9 + options.getLc()); - int dictSize = options.getDictSize(); - byte[] o = new byte[5]; + final int dictSize = options.getDictSize(); + final byte[] o = new byte[5]; o[0] = props; ByteUtils.toLittleEndian(o, dictSize, 1, 4); return o; @@ -71,13 +85,19 @@ byte[] getOptionsAsProperties(final Object opts) throws IOException { @Override Object getOptionsFromCoder(final Coder coder, final InputStream in) throws IOException { + if (coder.properties == null) { + throw new IOException("Missing LZMA properties"); + } + if (coder.properties.length < 1) { + throw new IOException("LZMA properties too short"); + } final byte propsByte = coder.properties[0]; int props = propsByte & 0xFF; - int pb = props / (9 * 5); + final int pb = props / (9 * 5); props -= pb * 9 * 5; - int lp = props / 9; - int lc = props - lp * 9; - LZMA2Options opts = new LZMA2Options(); + final int lp = props / 9; + final int lc = props - lp * 9; + final LZMA2Options opts = new LZMA2Options(); opts.setPb(pb); opts.setLcLp(lc, lp); opts.setDictSize(getDictionarySize(coder)); diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZArchiveEntry.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZArchiveEntry.java index d69ceaf5d3..0f3e628bd7 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZArchiveEntry.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZArchiveEntry.java @@ -20,10 +20,13 @@ package com.amaze.filemanager.filesystem.compressed.sevenz; +import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; +import java.util.Iterator; import java.util.LinkedList; +import java.util.Objects; import java.util.TimeZone; import org.apache.commons.compress.archivers.ArchiveEntry; @@ -50,6 +53,7 @@ public class SevenZArchiveEntry implements ArchiveEntry { private long crc, compressedCrc; private long size, compressedSize; private Iterable contentMethods; + static final SevenZArchiveEntry[] EMPTY_SEVEN_Z_ARCHIVE_ENTRY_ARRAY = new SevenZArchiveEntry[0]; public SevenZArchiveEntry() {} @@ -369,7 +373,7 @@ public void setCrc(final int crc) { /** * Gets the CRC. * - * @since Compress 1.7 + * @since 1.7 * @return the CRC */ public long getCrcValue() { @@ -379,7 +383,7 @@ public long getCrcValue() { /** * Sets the CRC. * - * @since Compress 1.7 + * @since 1.7 * @param crc the CRC */ public void setCrcValue(final long crc) { @@ -411,7 +415,7 @@ void setCompressedCrc(final int crc) { /** * Gets the compressed CRC. * - * @since Compress 1.7 + * @since 1.7 * @return the CRC */ long getCompressedCrcValue() { @@ -421,7 +425,7 @@ long getCompressedCrcValue() { /** * Sets the compressed CRC. * - * @since Compress 1.7 + * @since 1.7 * @param crc the CRC */ void setCompressedCrcValue(final long crc) { @@ -488,6 +492,21 @@ public void setContentMethods(final IterableCurrently only {@link SevenZMethod#COPY}, {@link SevenZMethod#LZMA2}, {@link + * SevenZMethod#BZIP2} and {@link SevenZMethod#DEFLATE} are supported when writing archives. + * + *

The methods will be consulted in iteration order to create the final output. + * + * @param methods the methods to use for the content + * @since 1.22 + */ + public void setContentMethods(SevenZMethodConfiguration... methods) { + setContentMethods(Arrays.asList(methods)); + } + /** * Gets the (compression) methods to use for entry's content - the default is LZMA2. * @@ -503,6 +522,41 @@ public Iterable getContentMethods() { return contentMethods; } + @Override + public int hashCode() { + final String n = getName(); + return n == null ? 0 : n.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final SevenZArchiveEntry other = (SevenZArchiveEntry) obj; + return Objects.equals(name, other.name) + && hasStream == other.hasStream + && isDirectory == other.isDirectory + && isAntiItem == other.isAntiItem + && hasCreationDate == other.hasCreationDate + && hasLastModifiedDate == other.hasLastModifiedDate + && hasAccessDate == other.hasAccessDate + && creationDate == other.creationDate + && lastModifiedDate == other.lastModifiedDate + && accessDate == other.accessDate + && hasWindowsAttributes == other.hasWindowsAttributes + && windowsAttributes == other.windowsAttributes + && hasCrc == other.hasCrc + && crc == other.crc + && compressedCrc == other.compressedCrc + && size == other.size + && compressedSize == other.compressedSize + && equalSevenZMethods(contentMethods, other.contentMethods); + } + /** * Converts NTFS time (100 nanosecond units since 1 January 1601) to Java time. * @@ -531,4 +585,25 @@ public static long javaTimeToNtfsTime(final Date date) { ntfsEpoch.set(Calendar.MILLISECOND, 0); return ((date.getTime() - ntfsEpoch.getTimeInMillis()) * 1000 * 10); } + + private boolean equalSevenZMethods( + final Iterable c1, + final Iterable c2) { + if (c1 == null) { + return c2 == null; + } + if (c2 == null) { + return false; + } + final Iterator i2 = c2.iterator(); + for (SevenZMethodConfiguration element : c1) { + if (!i2.hasNext()) { + return false; + } + if (!element.equals(i2.next())) { + return false; + } + } + return !i2.hasNext(); + } } diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZFile.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZFile.java index d0fd38e8c5..6b0039da58 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZFile.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZFile.java @@ -24,6 +24,7 @@ import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.DataInputStream; +import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FilterInputStream; @@ -32,23 +33,27 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.CharBuffer; +import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.charset.Charset; -import java.nio.charset.CharsetEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; +import java.util.LinkedHashMap; import java.util.LinkedList; +import java.util.List; +import java.util.Map; import java.util.zip.CRC32; +import java.util.zip.CheckedInputStream; +import org.apache.commons.compress.MemoryLimitException; import org.apache.commons.compress.utils.BoundedInputStream; +import org.apache.commons.compress.utils.ByteUtils; import org.apache.commons.compress.utils.CRC32VerifyingInputStream; -import org.apache.commons.compress.utils.CharsetNames; -import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.utils.InputStreamStatistics; /** - * Reads a 7z file, using FileChannel under the covers. + * Reads a 7z file, using SeekableByteChannel under the covers. * *

The 7z file format is a flexible container that can contain many compression and encryption * types, but at the moment only only Copy, LZMA, LZMA2, BZIP2, Deflate and AES-256 + SHA-256 are @@ -61,20 +66,27 @@ * *

Both the header and file contents may be compressed and/or encrypted. With both encrypted, * neither file names nor file contents can be read, but the use of encryption isn't plausibly - * deniable. @NotThreadSafe + * deniable. + * + *

Multi volume archives can be read by concatenating the parts in correct order - either + * manually or by using {link org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel} + * for example. @NotThreadSafe * * @since 1.6 */ public class SevenZFile implements Closeable { static final int SIGNATURE_HEADER_SIZE = 32; + private static final String DEFAULT_FILE_NAME = "unknown archive"; + private final String fileName; private FileChannel channel; private final Archive archive; private int currentEntryIndex = -1; private int currentFolderIndex = -1; - private InputStream currentFolderInputStream = null; + private InputStream currentFolderInputStream; private byte[] password; + private final SevenZFileOptions options; private long compressedBytesReadFromCurrentEntry; private long uncompressedBytesReadFromCurrentEntry; @@ -89,34 +101,55 @@ public class SevenZFile implements Closeable { /** * Reads a file as 7z archive * - * @param filename the file to read + * @param fileName the file to read * @param password optional password if the archive is encrypted * @throws IOException if reading the archive fails * @since 1.17 */ - public SevenZFile(final File filename, final char[] password) throws IOException { + public SevenZFile(final File fileName, final char[] password) throws IOException { + this(fileName, password, SevenZFileOptions.DEFAULT); + } + + /** + * Reads a file as 7z archive with additional options. + * + * @param fileName the file to read + * @param password optional password if the archive is encrypted + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile(final File fileName, final char[] password, final SevenZFileOptions options) + throws IOException { this( - new FileInputStream(filename).getChannel(), - filename.getAbsolutePath(), + new FileInputStream(fileName).getChannel(), // NOSONAR + fileName.getAbsolutePath(), utf16Decode(password), - true); + true, + options); } /** * Reads a file as 7z archive * - * @param filename the file to read + * @param fileName the file to read * @param password optional password if the archive is encrypted - the byte array is supposed to * be the UTF16-LE encoded representation of the password. * @throws IOException if reading the archive fails * @deprecated use the char[]-arg version for the password instead */ - public SevenZFile(final File filename, final byte[] password) throws IOException { - this(new FileInputStream(filename).getChannel(), filename.getAbsolutePath(), password, true); + @Deprecated + public SevenZFile(final File fileName, final byte[] password) throws IOException { + this( + new FileInputStream(fileName).getChannel(), + fileName.getAbsolutePath(), + password, + true, + SevenZFileOptions.DEFAULT); } /** - * Reads a FileChannel as 7z archive + * Reads a SeekableByteChannel as 7z archive * *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read * from an in-memory archive. @@ -126,11 +159,26 @@ public SevenZFile(final File filename, final byte[] password) throws IOException * @since 1.13 */ public SevenZFile(final FileChannel channel) throws IOException { - this(channel, "unknown archive", (char[]) null); + this(channel, SevenZFileOptions.DEFAULT); } /** - * Reads a FileChannel as 7z archive + * Reads a SeekableByteChannel as 7z archive with addtional options. + * + *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read + * from an in-memory archive. + * + * @param channel the channel to read + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile(final FileChannel channel, final SevenZFileOptions options) throws IOException { + this(channel, DEFAULT_FILE_NAME, null, options); + } + + /** + * Reads a SeekableByteChannel as 7z archive * *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read * from an in-memory archive. @@ -141,43 +189,101 @@ public SevenZFile(final FileChannel channel) throws IOException { * @since 1.17 */ public SevenZFile(final FileChannel channel, final char[] password) throws IOException { - this(channel, "unknown archive", utf16Decode(password)); + this(channel, password, SevenZFileOptions.DEFAULT); + } + + /** + * Reads a SeekableByteChannel as 7z archive with additional options. + * + *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read + * from an in-memory archive. + * + * @param channel the channel to read + * @param password optional password if the archive is encrypted + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile( + final FileChannel channel, final char[] password, final SevenZFileOptions options) + throws IOException { + this(channel, DEFAULT_FILE_NAME, password, options); } /** - * Reads a FileChannel as 7z archive + * Reads a SeekableByteChannel as 7z archive * *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read * from an in-memory archive. * * @param channel the channel to read - * @param filename name of the archive - only used for error reporting + * @param fileName name of the archive - only used for error reporting * @param password optional password if the archive is encrypted * @throws IOException if reading the archive fails * @since 1.17 */ - public SevenZFile(final FileChannel channel, String filename, final char[] password) + public SevenZFile(final FileChannel channel, final String fileName, final char[] password) + throws IOException { + this(channel, fileName, password, SevenZFileOptions.DEFAULT); + } + + /** + * Reads a SeekableByteChannel as 7z archive with addtional options. + * + *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read + * from an in-memory archive. + * + * @param channel the channel to read + * @param fileName name of the archive - only used for error reporting + * @param password optional password if the archive is encrypted + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile( + final FileChannel channel, + final String fileName, + final char[] password, + final SevenZFileOptions options) throws IOException { - this(channel, filename, utf16Decode(password), false); + this(channel, fileName, utf16Decode(password), false, options); } /** - * Reads a FileChannel as 7z archive + * Reads a SeekableByteChannel as 7z archive * *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read * from an in-memory archive. * * @param channel the channel to read - * @param filename name of the archive - only used for error reporting + * @param fileName name of the archive - only used for error reporting * @throws IOException if reading the archive fails * @since 1.17 */ - public SevenZFile(final FileChannel channel, String filename) throws IOException { - this(channel, filename, null, false); + public SevenZFile(final FileChannel channel, final String fileName) throws IOException { + this(channel, fileName, SevenZFileOptions.DEFAULT); } /** - * Reads a FileChannel as 7z archive + * Reads a SeekableByteChannel as 7z archive with additional options. + * + *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read + * from an in-memory archive. + * + * @param channel the channel to read + * @param fileName name of the archive - only used for error reporting + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile( + final FileChannel channel, final String fileName, final SevenZFileOptions options) + throws IOException { + this(channel, fileName, null, false, options); + } + + /** + * Reads a SeekableByteChannel as 7z archive * *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read * from an in-memory archive. @@ -189,35 +295,42 @@ public SevenZFile(final FileChannel channel, String filename) throws IOException * @since 1.13 * @deprecated use the char[]-arg version for the password instead */ + @Deprecated public SevenZFile(final FileChannel channel, final byte[] password) throws IOException { - this(channel, "unknown archive", password); + this(channel, DEFAULT_FILE_NAME, password); } /** - * Reads a FileChannel as 7z archive + * Reads a SeekableByteChannel as 7z archive * *

{@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read * from an in-memory archive. * * @param channel the channel to read - * @param filename name of the archive - only used for error reporting + * @param fileName name of the archive - only used for error reporting * @param password optional password if the archive is encrypted - the byte array is supposed to * be the UTF16-LE encoded representation of the password. * @throws IOException if reading the archive fails * @since 1.13 * @deprecated use the char[]-arg version for the password instead */ - public SevenZFile(final FileChannel channel, String filename, final byte[] password) + @Deprecated + public SevenZFile(final FileChannel channel, final String fileName, final byte[] password) throws IOException { - this(channel, filename, password, false); + this(channel, fileName, password, false, SevenZFileOptions.DEFAULT); } private SevenZFile( - final FileChannel channel, String filename, final byte[] password, boolean closeOnError) + final FileChannel channel, + final String filename, + final byte[] password, + final boolean closeOnError, + final SevenZFileOptions options) throws IOException { boolean succeeded = false; this.channel = channel; this.fileName = filename; + this.options = options; try { archive = readHeaders(password); if (password != null) { @@ -236,11 +349,23 @@ private SevenZFile( /** * Reads a file as unencrypted 7z archive * - * @param filename the file to read + * @param fileName the file to read * @throws IOException if reading the archive fails */ - public SevenZFile(final File filename) throws IOException { - this(filename, (char[]) null); + public SevenZFile(final File fileName) throws IOException { + this(fileName, SevenZFileOptions.DEFAULT); + } + + /** + * Reads a file as unencrypted 7z archive + * + * @param fileName the file to read + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile(final File fileName, final SevenZFileOptions options) throws IOException { + this(fileName, null, options); } /** @@ -275,13 +400,16 @@ public SevenZArchiveEntry getNextEntry() throws IOException { } ++currentEntryIndex; final SevenZArchiveEntry entry = archive.files[currentEntryIndex]; - buildDecodingStream(); + if (entry.getName() == null && options.getUseDefaultNameForUnnamedEntries()) { + entry.setName(getDefaultName()); + } + buildDecodingStream(currentEntryIndex, false); uncompressedBytesReadFromCurrentEntry = compressedBytesReadFromCurrentEntry = 0; return entry; } /** - * Returns meta-data of all archive entries. + * Returns a copy of meta-data of all archive entries. * *

This method only provides meta-data, the entries can not be used to read the contents, you * still need to process all entries in order using {@link #getNextEntry} for that. @@ -289,15 +417,15 @@ public SevenZArchiveEntry getNextEntry() throws IOException { *

The content methods are only available for entries that have already been reached via {@link * #getNextEntry}. * - * @return meta-data of all archive entries. + * @return a copy of meta-data of all archive entries. * @since 1.11 */ public Iterable getEntries() { - return Arrays.asList(archive.files); + return new ArrayList<>(Arrays.asList(archive.files)); } private Archive readHeaders(final byte[] password) throws IOException { - ByteBuffer buf = + final ByteBuffer buf = ByteBuffer.allocate(12 /* signature + 2 bytes version + 4 bytes CRC */) .order(ByteOrder.LITTLE_ENDIAN); readFully(buf); @@ -315,23 +443,104 @@ private Archive readHeaders(final byte[] password) throws IOException { "Unsupported 7z version (%d,%d)", archiveVersionMajor, archiveVersionMinor)); } + boolean headerLooksValid = + false; // See https://www.7-zip.org/recover.html - "There is no correct End Header at the + // end of archive" final long startHeaderCrc = 0xffffFFFFL & buf.getInt(); - final StartHeader startHeader = readStartHeader(startHeaderCrc); + if (startHeaderCrc == 0) { + // This is an indication of a corrupt header - peek the next 20 bytes + final long currentPosition = channel.position(); + final ByteBuffer peekBuf = ByteBuffer.allocate(20); + readFully(peekBuf); + channel.position(currentPosition); + // Header invalid if all data is 0 + while (peekBuf.hasRemaining()) { + if (peekBuf.get() != 0) { + headerLooksValid = true; + break; + } + } + } else { + headerLooksValid = true; + } - final int nextHeaderSizeInt = (int) startHeader.nextHeaderSize; - if (nextHeaderSizeInt != startHeader.nextHeaderSize) { - throw new IOException("cannot handle nextHeaderSize " + startHeader.nextHeaderSize); + if (headerLooksValid) { + return initializeArchive(readStartHeader(startHeaderCrc), password, true); } - channel.position(SIGNATURE_HEADER_SIZE + startHeader.nextHeaderOffset); - buf = ByteBuffer.allocate(nextHeaderSizeInt).order(ByteOrder.LITTLE_ENDIAN); - readFully(buf); - final CRC32 crc = new CRC32(); - crc.update(buf.array()); - if (startHeader.nextHeaderCrc != crc.getValue()) { - throw new IOException("NextHeader CRC mismatch"); + // No valid header found - probably first file of multipart archive was removed too early. Scan + // for end header. + if (options.getTryToRecoverBrokenArchives()) { + return tryToLocateEndHeader(password); } + throw new IOException( + "archive seems to be invalid.\nYou may want to retry and enable the" + + " tryToRecoverBrokenArchives if the archive could be a multi volume archive that has been closed" + + " prematurely."); + } + + private Archive tryToLocateEndHeader(final byte[] password) throws IOException { + final ByteBuffer nidBuf = ByteBuffer.allocate(1); + final long searchLimit = 1024L * 1024 * 1; + // Main header, plus bytes that readStartHeader would read + final long previousDataSize = channel.position() + 20; + final long minPos; + // Determine minimal position - can't start before current position + if (channel.position() + searchLimit > channel.size()) { + minPos = channel.position(); + } else { + minPos = channel.size() - searchLimit; + } + long pos = channel.size() - 1; + // Loop: Try from end of archive + while (pos > minPos) { + pos--; + channel.position(pos); + nidBuf.rewind(); + if (channel.read(nidBuf) < 1) { + throw new EOFException(); + } + final int nid = nidBuf.array()[0]; + // First indicator: Byte equals one of these header identifiers + if (nid == NID.kEncodedHeader || nid == NID.kHeader) { + try { + // Try to initialize Archive structure from here + final StartHeader startHeader = new StartHeader(); + startHeader.nextHeaderOffset = pos - previousDataSize; + startHeader.nextHeaderSize = channel.size() - pos; + final Archive result = initializeArchive(startHeader, password, false); + // Sanity check: There must be some data... + if (result.packSizes.length > 0 && result.files.length > 0) { + return result; + } + } catch (final Exception ignore) { + // Wrong guess... + } + } + } + throw new IOException("Start header corrupt and unable to guess end header"); + } + private Archive initializeArchive( + final StartHeader startHeader, final byte[] password, final boolean verifyCrc) + throws IOException { + assertFitsIntoNonNegativeInt("nextHeaderSize", startHeader.nextHeaderSize); + final int nextHeaderSizeInt = (int) startHeader.nextHeaderSize; + channel.position(SIGNATURE_HEADER_SIZE + startHeader.nextHeaderOffset); + if (verifyCrc) { + final long position = channel.position(); + CheckedInputStream cis = + new CheckedInputStream(Channels.newInputStream(channel), new CRC32()); + if (cis.skip(nextHeaderSizeInt) != nextHeaderSizeInt) { + throw new IOException("Problem computing NextHeader CRC-32"); + } + if (startHeader.nextHeaderCrc != cis.getChecksum().getValue()) { + throw new IOException("NextHeader CRC-32 mismatch"); + } + channel.position(position); + } Archive archive = new Archive(); + ByteBuffer buf = ByteBuffer.allocate(nextHeaderSizeInt).order(ByteOrder.LITTLE_ENDIAN); + readFully(buf); int nid = getUnsignedByte(buf); if (nid == NID.kEncodedHeader) { buf = readEncodedHeader(buf, archive, password); @@ -339,11 +548,11 @@ private Archive readHeaders(final byte[] password) throws IOException { archive = new Archive(); nid = getUnsignedByte(buf); } - if (nid == NID.kHeader) { - readHeader(buf, archive); - } else { + if (nid != NID.kHeader) { throw new IOException("Broken or unsupported archive: no Header"); } + readHeader(buf, archive); + archive.subStreamsInfo = null; return archive; } @@ -351,17 +560,35 @@ private StartHeader readStartHeader(final long startHeaderCrc) throws IOExceptio final StartHeader startHeader = new StartHeader(); // using Stream rather than ByteBuffer for the benefit of the // built-in CRC check - DataInputStream dataInputStream = + try (DataInputStream dataInputStream = new DataInputStream( new CRC32VerifyingInputStream( - new BoundedFileChannelInputStream(channel, 20), 20, startHeaderCrc)); - startHeader.nextHeaderOffset = Long.reverseBytes(dataInputStream.readLong()); - startHeader.nextHeaderSize = Long.reverseBytes(dataInputStream.readLong()); - startHeader.nextHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(dataInputStream.readInt()); - return startHeader; + new BoundedFileChannelInputStream(channel, 20), 20, startHeaderCrc))) { + startHeader.nextHeaderOffset = Long.reverseBytes(dataInputStream.readLong()); + if (startHeader.nextHeaderOffset < 0 + || startHeader.nextHeaderOffset + SIGNATURE_HEADER_SIZE > channel.size()) { + throw new IOException("nextHeaderOffset is out of bounds"); + } + + startHeader.nextHeaderSize = Long.reverseBytes(dataInputStream.readLong()); + final long nextHeaderEnd = startHeader.nextHeaderOffset + startHeader.nextHeaderSize; + if (nextHeaderEnd < startHeader.nextHeaderOffset + || nextHeaderEnd + SIGNATURE_HEADER_SIZE > channel.size()) { + throw new IOException("nextHeaderSize is out of bounds"); + } + + startHeader.nextHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(dataInputStream.readInt()); + + return startHeader; + } } private void readHeader(final ByteBuffer header, final Archive archive) throws IOException { + final int pos = header.position(); + final ArchiveStatistics stats = sanityCheckAndCollectStatistics(header); + stats.assertValidity(options.getMaxMemoryLimitInKb()); + header.position(pos); + int nid = getUnsignedByte(header); if (nid == NID.kArchiveProperties) { @@ -371,7 +598,7 @@ private void readHeader(final ByteBuffer header, final Archive archive) throws I if (nid == NID.kAdditionalStreamsInfo) { throw new IOException("Additional streams unsupported"); - // nid = header.readUnsignedByte(); + // nid = getUnsignedByte(header); } if (nid == NID.kMainStreamsInfo) { @@ -383,10 +610,39 @@ private void readHeader(final ByteBuffer header, final Archive archive) throws I readFilesInfo(header, archive); nid = getUnsignedByte(header); } + } + + private ArchiveStatistics sanityCheckAndCollectStatistics(final ByteBuffer header) + throws IOException { + final ArchiveStatistics stats = new ArchiveStatistics(); + + int nid = getUnsignedByte(header); + + if (nid == NID.kArchiveProperties) { + sanityCheckArchiveProperties(header); + nid = getUnsignedByte(header); + } + + if (nid == NID.kAdditionalStreamsInfo) { + throw new IOException("Additional streams unsupported"); + // nid = getUnsignedByte(header); + } + + if (nid == NID.kMainStreamsInfo) { + sanityCheckStreamsInfo(header, stats); + nid = getUnsignedByte(header); + } + + if (nid == NID.kFilesInfo) { + sanityCheckFilesInfo(header, stats); + nid = getUnsignedByte(header); + } if (nid != NID.kEnd) { throw new IOException("Badly terminated header, found " + nid); } + + return stats; } private void readArchiveProperties(final ByteBuffer input) throws IOException { @@ -395,15 +651,39 @@ private void readArchiveProperties(final ByteBuffer input) throws IOException { while (nid != NID.kEnd) { final long propertySize = readUint64(input); final byte[] property = new byte[(int) propertySize]; - input.get(property); + get(input, property); nid = getUnsignedByte(input); } } + private void sanityCheckArchiveProperties(final ByteBuffer header) throws IOException { + int nid = getUnsignedByte(header); + while (nid != NID.kEnd) { + final int propertySize = assertFitsIntoNonNegativeInt("propertySize", readUint64(header)); + if (skipBytesFully(header, propertySize) < propertySize) { + throw new IOException("invalid property size"); + } + nid = getUnsignedByte(header); + } + } + private ByteBuffer readEncodedHeader( final ByteBuffer header, final Archive archive, final byte[] password) throws IOException { + final int pos = header.position(); + final ArchiveStatistics stats = new ArchiveStatistics(); + sanityCheckStreamsInfo(header, stats); + stats.assertValidity(options.getMaxMemoryLimitInKb()); + header.position(pos); + readStreamsInfo(header, archive); + if (archive.folders == null || archive.folders.length == 0) { + throw new IOException("no folders, can't read encoded header"); + } + if (archive.packSizes == null || archive.packSizes.length == 0) { + throw new IOException("no packed streams, can't read encoded header"); + } + // FIXME: merge with buildDecodingStream()/buildDecoderStack() at some stage? final Folder folder = archive.folders[0]; final int firstPackStreamIndex = 0; @@ -422,18 +702,46 @@ private ByteBuffer readEncodedHeader( inputStreamStack, // NOSONAR folder.getUnpackSizeForCoder(coder), coder, - password); + password, + options.getMaxMemoryLimitInKb()); } if (folder.hasCrc) { inputStreamStack = new CRC32VerifyingInputStream(inputStreamStack, folder.getUnpackSize(), folder.crc); } - final byte[] nextHeader = new byte[(int) folder.getUnpackSize()]; - DataInputStream nextHeaderInputStream = new DataInputStream(inputStreamStack); - nextHeaderInputStream.readFully(nextHeader); + final int unpackSize = assertFitsIntoNonNegativeInt("unpackSize", folder.getUnpackSize()); + final byte[] nextHeader = IOUtils.readRange(inputStreamStack, unpackSize); + if (nextHeader.length < unpackSize) { + throw new IOException("premature end of stream"); + } + inputStreamStack.close(); return ByteBuffer.wrap(nextHeader).order(ByteOrder.LITTLE_ENDIAN); } + private void sanityCheckStreamsInfo(final ByteBuffer header, final ArchiveStatistics stats) + throws IOException { + int nid = getUnsignedByte(header); + + if (nid == NID.kPackInfo) { + sanityCheckPackInfo(header, stats); + nid = getUnsignedByte(header); + } + + if (nid == NID.kUnpackInfo) { + sanityCheckUnpackInfo(header, stats); + nid = getUnsignedByte(header); + } + + if (nid == NID.kSubStreamsInfo) { + sanityCheckSubStreamsInfo(header, stats); + nid = getUnsignedByte(header); + } + + if (nid != NID.kEnd) { + throw new IOException("Badly terminated StreamsInfo"); + } + } + private void readStreamsInfo(final ByteBuffer header, final Archive archive) throws IOException { int nid = getUnsignedByte(header); @@ -447,25 +755,58 @@ private void readStreamsInfo(final ByteBuffer header, final Archive archive) thr nid = getUnsignedByte(header); } else { // archive without unpack/coders info - archive.folders = new Folder[0]; + archive.folders = Folder.EMPTY_FOLDER_ARRAY; } if (nid == NID.kSubStreamsInfo) { readSubStreamsInfo(header, archive); nid = getUnsignedByte(header); } + } + + private void sanityCheckPackInfo(final ByteBuffer header, final ArchiveStatistics stats) + throws IOException { + final long packPos = readUint64(header); + if (packPos < 0 + || SIGNATURE_HEADER_SIZE + packPos > channel.size() + || SIGNATURE_HEADER_SIZE + packPos < 0) { + throw new IOException("packPos (" + packPos + ") is out of range"); + } + final long numPackStreams = readUint64(header); + stats.numberOfPackedStreams = assertFitsIntoNonNegativeInt("numPackStreams", numPackStreams); + int nid = getUnsignedByte(header); + if (nid == NID.kSize) { + long totalPackSizes = 0; + for (int i = 0; i < stats.numberOfPackedStreams; i++) { + final long packSize = readUint64(header); + totalPackSizes += packSize; + final long endOfPackStreams = SIGNATURE_HEADER_SIZE + packPos + totalPackSizes; + if (packSize < 0 || endOfPackStreams > channel.size() || endOfPackStreams < packPos) { + throw new IOException("packSize (" + packSize + ") is out of range"); + } + } + nid = getUnsignedByte(header); + } + + if (nid == NID.kCRC) { + final int crcsDefined = readAllOrBits(header, stats.numberOfPackedStreams).cardinality(); + if (skipBytesFully(header, 4 * crcsDefined) < 4 * crcsDefined) { + throw new IOException("invalid number of CRCs in PackInfo"); + } + nid = getUnsignedByte(header); + } if (nid != NID.kEnd) { - throw new IOException("Badly terminated StreamsInfo"); + throw new IOException("Badly terminated PackInfo (" + nid + ")"); } } private void readPackInfo(final ByteBuffer header, final Archive archive) throws IOException { archive.packPos = readUint64(header); - final long numPackStreams = readUint64(header); + final int numPackStreamsInt = (int) readUint64(header); int nid = getUnsignedByte(header); if (nid == NID.kSize) { - archive.packSizes = new long[(int) numPackStreams]; + archive.packSizes = new long[numPackStreamsInt]; for (int i = 0; i < archive.packSizes.length; i++) { archive.packSizes[i] = readUint64(header); } @@ -473,43 +814,84 @@ private void readPackInfo(final ByteBuffer header, final Archive archive) throws } if (nid == NID.kCRC) { - archive.packCrcsDefined = readAllOrBits(header, (int) numPackStreams); - archive.packCrcs = new long[(int) numPackStreams]; - for (int i = 0; i < (int) numPackStreams; i++) { + archive.packCrcsDefined = readAllOrBits(header, numPackStreamsInt); + archive.packCrcs = new long[numPackStreamsInt]; + for (int i = 0; i < numPackStreamsInt; i++) { if (archive.packCrcsDefined.get(i)) { - archive.packCrcs[i] = 0xffffFFFFL & header.getInt(); + archive.packCrcs[i] = 0xffffFFFFL & getInt(header); } } nid = getUnsignedByte(header); } - - if (nid != NID.kEnd) { - throw new IOException("Badly terminated PackInfo (" + nid + ")"); - } } - private void readUnpackInfo(final ByteBuffer header, final Archive archive) throws IOException { + private void sanityCheckUnpackInfo(final ByteBuffer header, final ArchiveStatistics stats) + throws IOException { int nid = getUnsignedByte(header); if (nid != NID.kFolder) { throw new IOException("Expected kFolder, got " + nid); } final long numFolders = readUint64(header); - final Folder[] folders = new Folder[(int) numFolders]; - archive.folders = folders; + stats.numberOfFolders = assertFitsIntoNonNegativeInt("numFolders", numFolders); final int external = getUnsignedByte(header); if (external != 0) { throw new IOException("External unsupported"); } - for (int i = 0; i < (int) numFolders; i++) { - folders[i] = readFolder(header); + + final List numberOfOutputStreamsPerFolder = new LinkedList<>(); + for (int i = 0; i < stats.numberOfFolders; i++) { + numberOfOutputStreamsPerFolder.add(sanityCheckFolder(header, stats)); + } + + final long totalNumberOfBindPairs = stats.numberOfOutStreams - stats.numberOfFolders; + final long packedStreamsRequiredByFolders = stats.numberOfInStreams - totalNumberOfBindPairs; + if (packedStreamsRequiredByFolders < stats.numberOfPackedStreams) { + throw new IOException("archive doesn't contain enough packed streams"); } nid = getUnsignedByte(header); if (nid != NID.kCodersUnpackSize) { throw new IOException("Expected kCodersUnpackSize, got " + nid); } + + for (final int numberOfOutputStreams : numberOfOutputStreamsPerFolder) { + for (int i = 0; i < numberOfOutputStreams; i++) { + final long unpackSize = readUint64(header); + if (unpackSize < 0) { + throw new IllegalArgumentException("negative unpackSize"); + } + } + } + + nid = getUnsignedByte(header); + if (nid == NID.kCRC) { + stats.folderHasCrc = readAllOrBits(header, stats.numberOfFolders); + final int crcsDefined = stats.folderHasCrc.cardinality(); + if (skipBytesFully(header, 4 * crcsDefined) < 4 * crcsDefined) { + throw new IOException("invalid number of CRCs in UnpackInfo"); + } + nid = getUnsignedByte(header); + } + + if (nid != NID.kEnd) { + throw new IOException("Badly terminated UnpackInfo"); + } + } + + private void readUnpackInfo(final ByteBuffer header, final Archive archive) throws IOException { + int nid = getUnsignedByte(header); + final int numFoldersInt = (int) readUint64(header); + final Folder[] folders = new Folder[numFoldersInt]; + archive.folders = folders; + /* final int external = */ getUnsignedByte(header); + for (int i = 0; i < numFoldersInt; i++) { + folders[i] = readFolder(header); + } + + nid = getUnsignedByte(header); for (final Folder folder : folders) { + assertFitsIntoNonNegativeInt("totalOutputStreams", folder.totalOutputStreams); folder.unpackSizes = new long[(int) folder.totalOutputStreams]; for (int i = 0; i < folder.totalOutputStreams; i++) { folder.unpackSizes[i] = readUint64(header); @@ -518,11 +900,11 @@ private void readUnpackInfo(final ByteBuffer header, final Archive archive) thro nid = getUnsignedByte(header); if (nid == NID.kCRC) { - final BitSet crcsDefined = readAllOrBits(header, (int) numFolders); - for (int i = 0; i < (int) numFolders; i++) { + final BitSet crcsDefined = readAllOrBits(header, numFoldersInt); + for (int i = 0; i < numFoldersInt; i++) { if (crcsDefined.get(i)) { folders[i].hasCrc = true; - folders[i].crc = 0xffffFFFFL & header.getInt(); + folders[i].crc = 0xffffFFFFL & getInt(header); } else { folders[i].hasCrc = false; } @@ -530,9 +912,71 @@ private void readUnpackInfo(final ByteBuffer header, final Archive archive) thro nid = getUnsignedByte(header); } + } + + private void sanityCheckSubStreamsInfo(final ByteBuffer header, final ArchiveStatistics stats) + throws IOException { + + int nid = getUnsignedByte(header); + final List numUnpackSubStreamsPerFolder = new LinkedList<>(); + if (nid == NID.kNumUnpackStream) { + for (int i = 0; i < stats.numberOfFolders; i++) { + numUnpackSubStreamsPerFolder.add( + assertFitsIntoNonNegativeInt("numStreams", readUint64(header))); + } + for (Integer n : numUnpackSubStreamsPerFolder) { + stats.numberOfUnpackSubStreams += n.longValue(); + } + nid = getUnsignedByte(header); + } else { + stats.numberOfUnpackSubStreams = stats.numberOfFolders; + } + + assertFitsIntoNonNegativeInt("totalUnpackStreams", stats.numberOfUnpackSubStreams); + + if (nid == NID.kSize) { + for (final int numUnpackSubStreams : numUnpackSubStreamsPerFolder) { + if (numUnpackSubStreams == 0) { + continue; + } + for (int i = 0; i < numUnpackSubStreams - 1; i++) { + final long size = readUint64(header); + if (size < 0) { + throw new IOException("negative unpackSize"); + } + } + } + nid = getUnsignedByte(header); + } + + int numDigests = 0; + if (numUnpackSubStreamsPerFolder.isEmpty()) { + numDigests = + stats.folderHasCrc == null + ? stats.numberOfFolders + : stats.numberOfFolders - stats.folderHasCrc.cardinality(); + } else { + int folderIdx = 0; + for (final int numUnpackSubStreams : numUnpackSubStreamsPerFolder) { + if (numUnpackSubStreams != 1 + || stats.folderHasCrc == null + || !stats.folderHasCrc.get(folderIdx++)) { + numDigests += numUnpackSubStreams; + } + } + } + + if (nid == NID.kCRC) { + assertFitsIntoNonNegativeInt("numDigests", numDigests); + final int missingCrcs = readAllOrBits(header, numDigests).cardinality(); + if (skipBytesFully(header, 4 * missingCrcs) < 4 * missingCrcs) { + throw new IOException("invalid number of missing CRCs in SubStreamInfo"); + } + nid = getUnsignedByte(header); + } if (nid != NID.kEnd) { - throw new IOException("Badly terminated UnpackInfo"); + throw new IOException("Badly terminated SubStreamsInfo"); } } @@ -541,19 +985,20 @@ private void readSubStreamsInfo(final ByteBuffer header, final Archive archive) for (final Folder folder : archive.folders) { folder.numUnpackSubStreams = 1; } - int totalUnpackStreams = archive.folders.length; + long unpackStreamsCount = archive.folders.length; int nid = getUnsignedByte(header); if (nid == NID.kNumUnpackStream) { - totalUnpackStreams = 0; + unpackStreamsCount = 0; for (final Folder folder : archive.folders) { final long numStreams = readUint64(header); folder.numUnpackSubStreams = (int) numStreams; - totalUnpackStreams += numStreams; + unpackStreamsCount += numStreams; } nid = getUnsignedByte(header); } + final int totalUnpackStreams = (int) unpackStreamsCount; final SubStreamsInfo subStreamsInfo = new SubStreamsInfo(); subStreamsInfo.unpackSizes = new long[totalUnpackStreams]; subStreamsInfo.hasCrc = new BitSet(totalUnpackStreams); @@ -572,6 +1017,9 @@ private void readSubStreamsInfo(final ByteBuffer header, final Archive archive) sum += size; } } + if (sum > folder.getUnpackSize()) { + throw new IOException("sum of unpack sizes of folder exceeds total unpack size"); + } subStreamsInfo.unpackSizes[nextUnpackStream++] = folder.getUnpackSize() - sum; } if (nid == NID.kSize) { @@ -590,7 +1038,7 @@ private void readSubStreamsInfo(final ByteBuffer header, final Archive archive) final long[] missingCrcs = new long[numDigests]; for (int i = 0; i < numDigests; i++) { if (hasMissingCrc.get(i)) { - missingCrcs[i] = 0xffffFFFFL & header.getInt(); + missingCrcs[i] = 0xffffFFFFL & getInt(header); } } int nextCrc = 0; @@ -610,14 +1058,98 @@ private void readSubStreamsInfo(final ByteBuffer header, final Archive archive) } } - nid = getUnsignedByte(header); + nid = getUnsignedByte(header); + } + + archive.subStreamsInfo = subStreamsInfo; + } + + private int sanityCheckFolder(final ByteBuffer header, final ArchiveStatistics stats) + throws IOException { + + final int numCoders = assertFitsIntoNonNegativeInt("numCoders", readUint64(header)); + if (numCoders == 0) { + throw new IOException("Folder without coders"); + } + stats.numberOfCoders += numCoders; + + long totalOutStreams = 0; + long totalInStreams = 0; + for (int i = 0; i < numCoders; i++) { + final int bits = getUnsignedByte(header); + final int idSize = bits & 0xf; + get(header, new byte[idSize]); + + final boolean isSimple = (bits & 0x10) == 0; + final boolean hasAttributes = (bits & 0x20) != 0; + final boolean moreAlternativeMethods = (bits & 0x80) != 0; + if (moreAlternativeMethods) { + throw new IOException( + "Alternative methods are unsupported, please report. " + + // NOSONAR + "The reference implementation doesn't support them either."); + } + + if (isSimple) { + totalInStreams++; + totalOutStreams++; + } else { + totalInStreams += assertFitsIntoNonNegativeInt("numInStreams", readUint64(header)); + totalOutStreams += assertFitsIntoNonNegativeInt("numOutStreams", readUint64(header)); + } + + if (hasAttributes) { + final int propertiesSize = + assertFitsIntoNonNegativeInt("propertiesSize", readUint64(header)); + if (skipBytesFully(header, propertiesSize) < propertiesSize) { + throw new IOException("invalid propertiesSize in folder"); + } + } + } + assertFitsIntoNonNegativeInt("totalInStreams", totalInStreams); + assertFitsIntoNonNegativeInt("totalOutStreams", totalOutStreams); + stats.numberOfOutStreams += totalOutStreams; + stats.numberOfInStreams += totalInStreams; + + if (totalOutStreams == 0) { + throw new IOException("Total output streams can't be 0"); } - if (nid != NID.kEnd) { - throw new IOException("Badly terminated SubStreamsInfo"); + final int numBindPairs = assertFitsIntoNonNegativeInt("numBindPairs", totalOutStreams - 1); + if (totalInStreams < numBindPairs) { + throw new IOException("Total input streams can't be less than the number of bind pairs"); + } + final BitSet inStreamsBound = new BitSet((int) totalInStreams); + for (int i = 0; i < numBindPairs; i++) { + final int inIndex = assertFitsIntoNonNegativeInt("inIndex", readUint64(header)); + if (totalInStreams <= inIndex) { + throw new IOException("inIndex is bigger than number of inStreams"); + } + inStreamsBound.set(inIndex); + final int outIndex = assertFitsIntoNonNegativeInt("outIndex", readUint64(header)); + if (totalOutStreams <= outIndex) { + throw new IOException("outIndex is bigger than number of outStreams"); + } } - archive.subStreamsInfo = subStreamsInfo; + final int numPackedStreams = + assertFitsIntoNonNegativeInt("numPackedStreams", totalInStreams - numBindPairs); + + if (numPackedStreams == 1) { + if (inStreamsBound.nextClearBit(0) == -1) { + throw new IOException("Couldn't find stream's bind pair index"); + } + } else { + for (int i = 0; i < numPackedStreams; i++) { + final int packedStreamIndex = + assertFitsIntoNonNegativeInt("packedStreamIndex", readUint64(header)); + if (packedStreamIndex >= totalInStreams) { + throw new IOException("packedStreamIndex is bigger than number of totalInStreams"); + } + } + } + + return (int) totalOutStreams; } private Folder readFolder(final ByteBuffer header) throws IOException { @@ -636,7 +1168,7 @@ private Folder readFolder(final ByteBuffer header) throws IOException { final boolean moreAlternativeMethods = (bits & 0x80) != 0; coders[i].decompressionMethodId = new byte[idSize]; - header.get(coders[i].decompressionMethodId); + get(header, coders[i].decompressionMethodId); if (isSimple) { coders[i].numInStreams = 1; coders[i].numOutStreams = 1; @@ -649,22 +1181,20 @@ private Folder readFolder(final ByteBuffer header) throws IOException { if (hasAttributes) { final long propertiesSize = readUint64(header); coders[i].properties = new byte[(int) propertiesSize]; - header.get(coders[i].properties); + get(header, coders[i].properties); } // would need to keep looping as above: - while (moreAlternativeMethods) { + if (moreAlternativeMethods) { throw new IOException( "Alternative methods are unsupported, please report. " - + "The reference implementation doesn't support them either."); + + // NOSONAR + "The reference implementation doesn't support them either."); } } folder.coders = coders; folder.totalInputStreams = totalInStreams; folder.totalOutputStreams = totalOutStreams; - if (totalOutStreams == 0) { - throw new IOException("Total output streams can't be 0"); - } final long numBindPairs = totalOutStreams - 1; final BindPair[] bindPairs = new BindPair[(int) numBindPairs]; for (int i = 0; i < bindPairs.length; i++) { @@ -674,11 +1204,8 @@ private Folder readFolder(final ByteBuffer header) throws IOException { } folder.bindPairs = bindPairs; - if (totalInStreams < numBindPairs) { - throw new IOException("Total input streams can't be less than the number of bind pairs"); - } final long numPackedStreams = totalInStreams - numBindPairs; - final long packedStreams[] = new long[(int) numPackedStreams]; + final long[] packedStreams = new long[(int) numPackedStreams]; if (numPackedStreams == 1) { int i; for (i = 0; i < (int) totalInStreams; i++) { @@ -686,9 +1213,6 @@ private Folder readFolder(final ByteBuffer header) throws IOException { break; } } - if (i == (int) totalInStreams) { - throw new IOException("Couldn't find stream's bind pair index"); - } packedStreams[0] = i; } else { for (int i = 0; i < (int) numPackedStreams; i++) { @@ -729,15 +1253,11 @@ private BitSet readBits(final ByteBuffer header, final int size) throws IOExcept return bits; } - private void readFilesInfo(final ByteBuffer header, final Archive archive) throws IOException { - final long numFiles = readUint64(header); - final SevenZArchiveEntry[] files = new SevenZArchiveEntry[(int) numFiles]; - for (int i = 0; i < files.length; i++) { - files[i] = new SevenZArchiveEntry(); - } - BitSet isEmptyStream = null; - BitSet isEmptyFile = null; - BitSet isAnti = null; + private void sanityCheckFilesInfo(final ByteBuffer header, final ArchiveStatistics stats) + throws IOException { + stats.numberOfEntries = assertFitsIntoNonNegativeInt("numFiles", readUint64(header)); + + int emptyStreams = -1; while (true) { final int propertyType = getUnsignedByte(header); if (propertyType == 0) { @@ -747,24 +1267,24 @@ private void readFilesInfo(final ByteBuffer header, final Archive archive) throw switch (propertyType) { case NID.kEmptyStream: { - isEmptyStream = readBits(header, files.length); + emptyStreams = readBits(header, stats.numberOfEntries).cardinality(); break; } case NID.kEmptyFile: { - if (isEmptyStream == null) { // protect against NPE + if (emptyStreams == -1) { throw new IOException( "Header format error: kEmptyStream must appear before kEmptyFile"); } - isEmptyFile = readBits(header, isEmptyStream.cardinality()); + readBits(header, emptyStreams); break; } case NID.kAnti: { - if (isEmptyStream == null) { // protect against NPE + if (emptyStreams == -1) { throw new IOException("Header format error: kEmptyStream must appear before kAnti"); } - isAnti = readBits(header, isEmptyStream.cardinality()); + readBits(header, emptyStreams); break; } case NID.kName: @@ -773,82 +1293,74 @@ private void readFilesInfo(final ByteBuffer header, final Archive archive) throw if (external != 0) { throw new IOException("Not implemented"); } - if (((size - 1) & 1) != 0) { + final int namesLength = assertFitsIntoNonNegativeInt("file names length", size - 1); + if ((namesLength & 1) != 0) { throw new IOException("File names length invalid"); } - final byte[] names = new byte[(int) (size - 1)]; - header.get(names); - int nextFile = 0; - int nextName = 0; - for (int i = 0; i < names.length; i += 2) { - if (names[i] == 0 && names[i + 1] == 0) { - files[nextFile++].setName( - new String(names, nextName, i - nextName, CharsetNames.UTF_16LE)); - nextName = i + 2; + + int filesSeen = 0; + for (int i = 0; i < namesLength; i += 2) { + final char c = getChar(header); + if (c == 0) { + filesSeen++; } } - if (nextName != names.length || nextFile != files.length) { - throw new IOException("Error parsing file names"); + if (filesSeen != stats.numberOfEntries) { + throw new IOException( + "Invalid number of file names (" + + filesSeen + + " instead of " + + stats.numberOfEntries + + ")"); } break; } case NID.kCTime: { - final BitSet timesDefined = readAllOrBits(header, files.length); + final int timesDefined = readAllOrBits(header, stats.numberOfEntries).cardinality(); final int external = getUnsignedByte(header); if (external != 0) { - throw new IOException("Unimplemented"); + throw new IOException("Not implemented"); } - for (int i = 0; i < files.length; i++) { - files[i].setHasCreationDate(timesDefined.get(i)); - if (files[i].getHasCreationDate()) { - files[i].setCreationDate(header.getLong()); - } + if (skipBytesFully(header, 8 * timesDefined) < 8 * timesDefined) { + throw new IOException("invalid creation dates size"); } break; } case NID.kATime: { - final BitSet timesDefined = readAllOrBits(header, files.length); + final int timesDefined = readAllOrBits(header, stats.numberOfEntries).cardinality(); final int external = getUnsignedByte(header); if (external != 0) { - throw new IOException("Unimplemented"); + throw new IOException("Not implemented"); } - for (int i = 0; i < files.length; i++) { - files[i].setHasAccessDate(timesDefined.get(i)); - if (files[i].getHasAccessDate()) { - files[i].setAccessDate(header.getLong()); - } + if (skipBytesFully(header, 8 * timesDefined) < 8 * timesDefined) { + throw new IOException("invalid access dates size"); } break; } case NID.kMTime: { - final BitSet timesDefined = readAllOrBits(header, files.length); + final int timesDefined = readAllOrBits(header, stats.numberOfEntries).cardinality(); final int external = getUnsignedByte(header); if (external != 0) { - throw new IOException("Unimplemented"); + throw new IOException("Not implemented"); } - for (int i = 0; i < files.length; i++) { - files[i].setHasLastModifiedDate(timesDefined.get(i)); - if (files[i].getHasLastModifiedDate()) { - files[i].setLastModifiedDate(header.getLong()); - } + if (skipBytesFully(header, 8 * timesDefined) < 8 * timesDefined) { + throw new IOException("invalid modification dates size"); } break; } case NID.kWinAttributes: { - final BitSet attributesDefined = readAllOrBits(header, files.length); + final int attributesDefined = + readAllOrBits(header, stats.numberOfEntries).cardinality(); final int external = getUnsignedByte(header); if (external != 0) { - throw new IOException("Unimplemented"); + throw new IOException("Not implemented"); } - for (int i = 0; i < files.length; i++) { - files[i].setHasWindowsAttributes(attributesDefined.get(i)); - if (files[i].getHasWindowsAttributes()) { - files[i].setWindowsAttributes(header.getInt()); - } + if (skipBytesFully(header, 4 * attributesDefined) < 4 * attributesDefined) { + throw new IOException("invalid windows attributes size"); } break; } @@ -877,29 +1389,179 @@ private void readFilesInfo(final ByteBuffer header, final Archive archive) throw } } } + stats.numberOfEntriesWithStream = stats.numberOfEntries - Math.max(emptyStreams, 0); + } + + private void readFilesInfo(final ByteBuffer header, final Archive archive) throws IOException { + final int numFilesInt = (int) readUint64(header); + final Map fileMap = new LinkedHashMap<>(); + BitSet isEmptyStream = null; + BitSet isEmptyFile = null; + BitSet isAnti = null; + while (true) { + final int propertyType = getUnsignedByte(header); + if (propertyType == 0) { + break; + } + final long size = readUint64(header); + switch (propertyType) { + case NID.kEmptyStream: + { + isEmptyStream = readBits(header, numFilesInt); + break; + } + case NID.kEmptyFile: + { + isEmptyFile = readBits(header, isEmptyStream.cardinality()); + break; + } + case NID.kAnti: + { + isAnti = readBits(header, isEmptyStream.cardinality()); + break; + } + case NID.kName: + { + /* final int external = */ getUnsignedByte(header); + final byte[] names = new byte[(int) (size - 1)]; + final int namesLength = names.length; + get(header, names); + int nextFile = 0; + int nextName = 0; + for (int i = 0; i < namesLength; i += 2) { + if (names[i] == 0 && names[i + 1] == 0) { + checkEntryIsInitialized(fileMap, nextFile); + fileMap + .get(nextFile) + .setName(new String(names, nextName, i - nextName, "UTF-16LE")); + nextName = i + 2; + nextFile++; + } + } + if (nextName != namesLength || nextFile != numFilesInt) { + throw new IOException("Error parsing file names"); + } + break; + } + case NID.kCTime: + { + final BitSet timesDefined = readAllOrBits(header, numFilesInt); + /* final int external = */ getUnsignedByte(header); + for (int i = 0; i < numFilesInt; i++) { + checkEntryIsInitialized(fileMap, i); + final SevenZArchiveEntry entryAtIndex = fileMap.get(i); + entryAtIndex.setHasCreationDate(timesDefined.get(i)); + if (entryAtIndex.getHasCreationDate()) { + entryAtIndex.setCreationDate(getLong(header)); + } + } + break; + } + case NID.kATime: + { + final BitSet timesDefined = readAllOrBits(header, numFilesInt); + /* final int external = */ getUnsignedByte(header); + for (int i = 0; i < numFilesInt; i++) { + checkEntryIsInitialized(fileMap, i); + final SevenZArchiveEntry entryAtIndex = fileMap.get(i); + entryAtIndex.setHasAccessDate(timesDefined.get(i)); + if (entryAtIndex.getHasAccessDate()) { + entryAtIndex.setAccessDate(getLong(header)); + } + } + break; + } + case NID.kMTime: + { + final BitSet timesDefined = readAllOrBits(header, numFilesInt); + /* final int external = */ getUnsignedByte(header); + for (int i = 0; i < numFilesInt; i++) { + checkEntryIsInitialized(fileMap, i); + final SevenZArchiveEntry entryAtIndex = fileMap.get(i); + entryAtIndex.setHasLastModifiedDate(timesDefined.get(i)); + if (entryAtIndex.getHasLastModifiedDate()) { + entryAtIndex.setLastModifiedDate(getLong(header)); + } + } + break; + } + case NID.kWinAttributes: + { + final BitSet attributesDefined = readAllOrBits(header, numFilesInt); + /* final int external = */ getUnsignedByte(header); + for (int i = 0; i < numFilesInt; i++) { + checkEntryIsInitialized(fileMap, i); + final SevenZArchiveEntry entryAtIndex = fileMap.get(i); + entryAtIndex.setHasWindowsAttributes(attributesDefined.get(i)); + if (entryAtIndex.getHasWindowsAttributes()) { + entryAtIndex.setWindowsAttributes(getInt(header)); + } + } + break; + } + case NID.kDummy: + { + // 7z 9.20 asserts the content is all zeros and ignores the property + // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287 + + skipBytesFully(header, size); + break; + } + + default: + { + // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287 + skipBytesFully(header, size); + break; + } + } + } int nonEmptyFileCounter = 0; int emptyFileCounter = 0; - for (int i = 0; i < files.length; i++) { - files[i].setHasStream(isEmptyStream == null || !isEmptyStream.get(i)); - if (files[i].hasStream()) { - files[i].setDirectory(false); - files[i].setAntiItem(false); - files[i].setHasCrc(archive.subStreamsInfo.hasCrc.get(nonEmptyFileCounter)); - files[i].setCrcValue(archive.subStreamsInfo.crcs[nonEmptyFileCounter]); - files[i].setSize(archive.subStreamsInfo.unpackSizes[nonEmptyFileCounter]); + for (int i = 0; i < numFilesInt; i++) { + final SevenZArchiveEntry entryAtIndex = fileMap.get(i); + if (entryAtIndex == null) { + continue; + } + entryAtIndex.setHasStream(isEmptyStream == null || !isEmptyStream.get(i)); + if (entryAtIndex.hasStream()) { + if (archive.subStreamsInfo == null) { + throw new IOException("Archive contains file with streams but no subStreamsInfo"); + } + entryAtIndex.setDirectory(false); + entryAtIndex.setAntiItem(false); + entryAtIndex.setHasCrc(archive.subStreamsInfo.hasCrc.get(nonEmptyFileCounter)); + entryAtIndex.setCrcValue(archive.subStreamsInfo.crcs[nonEmptyFileCounter]); + entryAtIndex.setSize(archive.subStreamsInfo.unpackSizes[nonEmptyFileCounter]); + if (entryAtIndex.getSize() < 0) { + throw new IOException("broken archive, entry with negative size"); + } ++nonEmptyFileCounter; } else { - files[i].setDirectory(isEmptyFile == null || !isEmptyFile.get(emptyFileCounter)); - files[i].setAntiItem(isAnti != null && isAnti.get(emptyFileCounter)); - files[i].setHasCrc(false); - files[i].setSize(0); + entryAtIndex.setDirectory(isEmptyFile == null || !isEmptyFile.get(emptyFileCounter)); + entryAtIndex.setAntiItem(isAnti != null && isAnti.get(emptyFileCounter)); + entryAtIndex.setHasCrc(false); + entryAtIndex.setSize(0); ++emptyFileCounter; } } - archive.files = files; + List entries = new LinkedList<>(); + for (SevenZArchiveEntry entry : fileMap.values()) { + if (entry != null) { + entries.add(entry); + } + } + archive.files = entries.toArray(new SevenZArchiveEntry[entries.size()]); calculateStreamMap(archive); } + private void checkEntryIsInitialized( + final Map archiveEntries, final int index) { + if (archiveEntries.get(index) == null) { + archiveEntries.put(index, new SevenZArchiveEntry()); + } + } + private void calculateStreamMap(final Archive archive) throws IOException { final StreamMap streamMap = new StreamMap(); @@ -912,7 +1574,7 @@ private void calculateStreamMap(final Archive archive) throws IOException { } long nextPackStreamOffset = 0; - final int numPackSizes = archive.packSizes != null ? archive.packSizes.length : 0; + final int numPackSizes = archive.packSizes.length; streamMap.packStreamOffsets = new long[numPackSizes]; for (int i = 0; i < numPackSizes; i++) { streamMap.packStreamOffsets[i] = nextPackStreamOffset; @@ -953,40 +1615,69 @@ private void calculateStreamMap(final Archive archive) throws IOException { archive.streamMap = streamMap; } - private void buildDecodingStream() throws IOException { - final int folderIndex = archive.streamMap.fileFolderIndex[currentEntryIndex]; + /** + * Build the decoding stream for the entry to be read. This method may be called from a random + * access(getInputStream) or sequential access(getNextEntry). If this method is called from a + * random access, some entries may need to be skipped(we put them to the deferredBlockStreams and + * skip them when actually needed to improve the performance) + * + * @param entryIndex the index of the entry to be read + * @param isRandomAccess is this called in a random access + * @throws IOException if there are exceptions when reading the file + */ + private void buildDecodingStream(final int entryIndex, final boolean isRandomAccess) + throws IOException { + if (archive.streamMap == null) { + throw new IOException("Archive doesn't contain stream information to read entries"); + } + final int folderIndex = archive.streamMap.fileFolderIndex[entryIndex]; if (folderIndex < 0) { deferredBlockStreams.clear(); // TODO: previously it'd return an empty stream? - // new BoundedInputStream(new ByteArrayInputStream(new byte[0]), 0); + // new BoundedInputStream(new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY), 0); return; } - final SevenZArchiveEntry file = archive.files[currentEntryIndex]; + final SevenZArchiveEntry file = archive.files[entryIndex]; + boolean isInSameFolder = false; if (currentFolderIndex == folderIndex) { // (COMPRESS-320). // The current entry is within the same (potentially opened) folder. The // previous stream has to be fully decoded before we can start reading // but don't do it eagerly -- if the user skips over the entire folder nothing // is effectively decompressed. + if (entryIndex > 0) { + file.setContentMethods(archive.files[entryIndex - 1].getContentMethods()); + } - file.setContentMethods(archive.files[currentEntryIndex - 1].getContentMethods()); + // if this is called in a random access, then the content methods of previous entry may be + // null + // the content methods should be set to methods of the first entry as it must not be null, + // and the content methods would only be set if the content methods was not set + if (isRandomAccess && file.getContentMethods() == null) { + final int folderFirstFileIndex = archive.streamMap.folderFirstFileIndex[folderIndex]; + final SevenZArchiveEntry folderFirstFile = archive.files[folderFirstFileIndex]; + file.setContentMethods(folderFirstFile.getContentMethods()); + } + isInSameFolder = true; } else { - // We're opening a new folder. Discard any queued streams/ folder stream. currentFolderIndex = folderIndex; - deferredBlockStreams.clear(); - if (currentFolderInputStream != null) { - currentFolderInputStream.close(); - currentFolderInputStream = null; - } + // We're opening a new folder. Discard any queued streams/ folder stream. + reopenFolderInputStream(folderIndex, file); + } + + boolean haveSkippedEntries = false; + if (isRandomAccess) { + // entries will only need to be skipped if it's a random access + haveSkippedEntries = skipEntriesWhenNeeded(entryIndex, isInSameFolder, folderIndex); + } - final Folder folder = archive.folders[folderIndex]; - final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex]; - final long folderOffset = - SIGNATURE_HEADER_SIZE - + archive.packPos - + archive.streamMap.packStreamOffsets[firstPackStreamIndex]; - currentFolderInputStream = - buildDecoderStack(folder, folderOffset, firstPackStreamIndex, file); + if (isRandomAccess && currentEntryIndex == entryIndex && !haveSkippedEntries) { + // we don't need to add another entry to the deferredBlockStreams when : + // 1. If this method is called in a random access and the entry index + // to be read equals to the current entry index, the input stream + // has already been put in the deferredBlockStreams + // 2. If this entry has not been read(which means no entries are skipped) + return; } InputStream fileStream = new BoundedInputStream(currentFolderInputStream, file.getSize()); @@ -997,6 +1688,128 @@ private void buildDecodingStream() throws IOException { deferredBlockStreams.add(fileStream); } + /** + * Discard any queued streams/ folder stream, and reopen the current folder input stream. + * + * @param folderIndex the index of the folder to reopen + * @param file the 7z entry to read + * @throws IOException if exceptions occur when reading the 7z file + */ + private void reopenFolderInputStream(final int folderIndex, final SevenZArchiveEntry file) + throws IOException { + deferredBlockStreams.clear(); + if (currentFolderInputStream != null) { + currentFolderInputStream.close(); + currentFolderInputStream = null; + } + final Folder folder = archive.folders[folderIndex]; + final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex]; + final long folderOffset = + SIGNATURE_HEADER_SIZE + + archive.packPos + + archive.streamMap.packStreamOffsets[firstPackStreamIndex]; + + currentFolderInputStream = buildDecoderStack(folder, folderOffset, firstPackStreamIndex, file); + } + + /** + * Skip all the entries if needed. Entries need to be skipped when: + * + *

1. it's a random access 2. one of these 2 condition is meet : + * + *

2.1 currentEntryIndex != entryIndex : this means there are some entries to be + * skipped(currentEntryIndex < entryIndex) or the entry has already been read(currentEntryIndex > + * entryIndex) + * + *

2.2 currentEntryIndex == entryIndex && !hasCurrentEntryBeenRead: if the entry to be read is + * the current entry, but some data of it has been read before, then we need to reopen the stream + * of the folder and skip all the entries before the current entries + * + * @param entryIndex the entry to be read + * @param isInSameFolder are the entry to be read and the current entry in the same folder + * @param folderIndex the index of the folder which contains the entry + * @return true if there are entries actually skipped + * @throws IOException there are exceptions when skipping entries + * @since 1.21 + */ + private boolean skipEntriesWhenNeeded( + final int entryIndex, final boolean isInSameFolder, final int folderIndex) + throws IOException { + final SevenZArchiveEntry file = archive.files[entryIndex]; + // if the entry to be read is the current entry, and the entry has not + // been read yet, then there's nothing we need to do + if (currentEntryIndex == entryIndex && !hasCurrentEntryBeenRead()) { + return false; + } + + // 1. if currentEntryIndex < entryIndex : + // this means there are some entries to be skipped(currentEntryIndex < entryIndex) + // 2. if currentEntryIndex > entryIndex || (currentEntryIndex == entryIndex && + // hasCurrentEntryBeenRead) : + // this means the entry has already been read before, and we need to reopen the + // stream of the folder and skip all the entries before the current entries + int filesToSkipStartIndex = archive.streamMap.folderFirstFileIndex[currentFolderIndex]; + if (isInSameFolder) { + if (currentEntryIndex < entryIndex) { + // the entries between filesToSkipStartIndex and currentEntryIndex had already been skipped + filesToSkipStartIndex = currentEntryIndex + 1; + } else { + // the entry is in the same folder of current entry, but it has already been read before, we + // need to reset + // the position of the currentFolderInputStream to the beginning of folder, and then skip + // the files + // from the start entry of the folder again + reopenFolderInputStream(folderIndex, file); + } + } + + for (int i = filesToSkipStartIndex; i < entryIndex; i++) { + final SevenZArchiveEntry fileToSkip = archive.files[i]; + InputStream fileStreamToSkip = + new BoundedInputStream(currentFolderInputStream, fileToSkip.getSize()); + if (fileToSkip.getHasCrc()) { + fileStreamToSkip = + new CRC32VerifyingInputStream( + fileStreamToSkip, fileToSkip.getSize(), fileToSkip.getCrcValue()); + } + deferredBlockStreams.add(fileStreamToSkip); + + // set the content methods as well, it equals to file.getContentMethods() because they are in + // same folder + fileToSkip.setContentMethods(file.getContentMethods()); + } + return true; + } + + /** + * Find out if any data of current entry has been read or not. This is achieved by comparing the + * bytes remaining to read and the size of the file. + * + * @return true if any data of current entry has been read + * @since 1.21 + */ + private boolean hasCurrentEntryBeenRead() { + boolean hasCurrentEntryBeenRead = false; + if (!deferredBlockStreams.isEmpty()) { + final InputStream currentEntryInputStream = + deferredBlockStreams.get(deferredBlockStreams.size() - 1); + // get the bytes remaining to read, and compare it with the size of + // the file to figure out if the file has been read + if (currentEntryInputStream instanceof CRC32VerifyingInputStream) { + hasCurrentEntryBeenRead = + ((CRC32VerifyingInputStream) currentEntryInputStream).getBytesRemaining() + != archive.files[currentEntryIndex].getSize(); + } + + if (currentEntryInputStream instanceof BoundedInputStream) { + hasCurrentEntryBeenRead = + ((BoundedInputStream) currentEntryInputStream).getBytesRemaining() + != archive.files[currentEntryIndex].getSize(); + } + } + return hasCurrentEntryBeenRead; + } + private InputStream buildDecoderStack( final Folder folder, final long folderOffset, @@ -1025,6 +1838,9 @@ public int read(final byte[] b) throws IOException { @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } final int r = in.read(b, off, len); if (r >= 0) { count(r); @@ -1032,7 +1848,7 @@ public int read(final byte[] b, final int off, final int len) throws IOException return r; } - private void count(int c) { + private void count(final int c) { compressedBytesReadFromCurrentEntry += c; } }; @@ -1044,7 +1860,12 @@ private void count(int c) { final SevenZMethod method = SevenZMethod.byId(coder.decompressionMethodId); inputStreamStack = Coders.addDecoder( - fileName, inputStreamStack, folder.getUnpackSizeForCoder(coder), coder, password); + fileName, + inputStreamStack, + folder.getUnpackSizeForCoder(coder), + coder, + password, + options.getMaxMemoryLimitInKb()); methods.addFirst( new SevenZMethodConfiguration( method, Coders.findByMethod(method).getOptionsFromCoder(coder, inputStreamStack))); @@ -1063,7 +1884,7 @@ private void count(int c) { * @throws IOException if an I/O error has occurred */ public int read() throws IOException { - int b = getCurrentStream().read(); + final int b = getCurrentStream().read(); if (b >= 0) { uncompressedBytesReadFromCurrentEntry++; } @@ -1072,7 +1893,7 @@ public int read() throws IOException { private InputStream getCurrentStream() throws IOException { if (archive.files[currentEntryIndex].getSize() == 0) { - return new ByteArrayInputStream(new byte[0]); + return new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY); } if (deferredBlockStreams.isEmpty()) { throw new IllegalStateException("No current 7z entry (call getNextEntry() first)."); @@ -1082,14 +1903,46 @@ private InputStream getCurrentStream() throws IOException { // In solid compression mode we need to decompress all leading folder' // streams to get access to an entry. We defer this until really needed // so that entire blocks can be skipped without wasting time for decompression. - final InputStream stream = deferredBlockStreams.remove(0); - IOUtils.skip(stream, Long.MAX_VALUE); + try (final InputStream stream = deferredBlockStreams.remove(0)) { + IOUtils.skip(stream, Long.MAX_VALUE); + } compressedBytesReadFromCurrentEntry = 0; } return deferredBlockStreams.get(0); } + /** + * Returns an InputStream for reading the contents of the given entry. + * + *

For archives using solid compression randomly accessing entries will be significantly slower + * than reading the archive sequentially. + * + * @param entry the entry to get the stream for. + * @return a stream to read the entry from. + * @throws IOException if unable to create an input stream from the zipentry + * @since 1.20 + */ + public InputStream getInputStream(final SevenZArchiveEntry entry) throws IOException { + int entryIndex = -1; + for (int i = 0; i < this.archive.files.length; i++) { + if (entry == this.archive.files[i]) { + entryIndex = i; + break; + } + } + + if (entryIndex < 0) { + throw new IllegalArgumentException( + "Can not find " + entry.getName() + " in " + this.fileName); + } + + buildDecodingStream(entryIndex, true); + currentEntryIndex = entryIndex; + currentFolderIndex = archive.streamMap.fileFolderIndex[entryIndex]; + return getCurrentStream(); + } + /** * Reads data into an array of bytes. * @@ -1111,7 +1964,10 @@ public int read(final byte[] b) throws IOException { * @throws IOException if an I/O error has occurred */ public int read(final byte[] b, final int off, final int len) throws IOException { - int cnt = getCurrentStream().read(b, off, len); + if (len == 0) { + return 0; + } + final int cnt = getCurrentStream().read(b, off, len); if (cnt > 0) { uncompressedBytesReadFromCurrentEntry += cnt; } @@ -1145,16 +2001,47 @@ private static long readUint64(final ByteBuffer in) throws IOException { long value = 0; for (int i = 0; i < 8; i++) { if ((firstByte & mask) == 0) { - return value | ((firstByte & (mask - 1)) << (8 * i)); + return value | (firstByte & mask - 1) << 8 * i; } final long nextByte = getUnsignedByte(in); - value |= nextByte << (8 * i); + value |= nextByte << 8 * i; mask >>>= 1; } return value; } - private static int getUnsignedByte(ByteBuffer buf) { + private static char getChar(final ByteBuffer buf) throws IOException { + if (buf.remaining() < 2) { + throw new EOFException(); + } + return buf.getChar(); + } + + private static int getInt(final ByteBuffer buf) throws IOException { + if (buf.remaining() < 4) { + throw new EOFException(); + } + return buf.getInt(); + } + + private static long getLong(final ByteBuffer buf) throws IOException { + if (buf.remaining() < 8) { + throw new EOFException(); + } + return buf.getLong(); + } + + private static void get(final ByteBuffer buf, final byte[] to) throws IOException { + if (buf.remaining() < to.length) { + throw new EOFException(); + } + buf.get(to); + } + + private static int getUnsignedByte(final ByteBuffer buf) throws IOException { + if (!buf.hasRemaining()) { + throw new EOFException(); + } return buf.get() & 0xff; } @@ -1179,12 +2066,12 @@ public static boolean matches(final byte[] signature, final int length) { return true; } - private static long skipBytesFully(final ByteBuffer input, long bytesToSkip) throws IOException { + private static long skipBytesFully(final ByteBuffer input, long bytesToSkip) { if (bytesToSkip < 1) { return 0; } - int current = input.position(); - int maxSkip = input.remaining(); + final int current = input.position(); + final int maxSkip = input.remaining(); if (maxSkip < bytesToSkip) { bytesToSkip = maxSkip; } @@ -1192,8 +2079,9 @@ private static long skipBytesFully(final ByteBuffer input, long bytesToSkip) thr return bytesToSkip; } - private void readFully(ByteBuffer buf) throws IOException { + private void readFully(final ByteBuffer buf) throws IOException { buf.rewind(); + IOUtils.readFully(channel, buf); buf.flip(); } @@ -1203,18 +2091,128 @@ public String toString() { return archive.toString(); } - private static final CharsetEncoder PASSWORD_ENCODER = Charset.forName("UTF-16LE").newEncoder(); + /** + * Derives a default file name from the archive name - if known. + * + *

This implements the same heuristics the 7z tools use. In 7z's case if an archive contains + * entries without a name - i.e. {@link SevenZArchiveEntry#getName} returns {@code null} - then + * its command line and GUI tools will use this default name when extracting the entries. + * + * @return null if the name of the archive is unknown. Otherwise if the name of the archive has + * got any extension, it is stripped and the remainder returned. Finally if the name of the + * archive hasn't got any extension then a {@code ~} character is appended to the archive + * name. + * @since 1.19 + */ + public String getDefaultName() { + if (DEFAULT_FILE_NAME.equals(fileName) || fileName == null) { + return null; + } + + final String lastSegment = new File(fileName).getName(); + final int dotPos = lastSegment.lastIndexOf("."); + if (dotPos > 0) { // if the file starts with a dot then this is not an extension + return lastSegment.substring(0, dotPos); + } + return lastSegment + "~"; + } - private static byte[] utf16Decode(char[] chars) throws IOException { + private static byte[] utf16Decode(final char[] chars) { if (chars == null) { return null; } - ByteBuffer encoded = PASSWORD_ENCODER.encode(CharBuffer.wrap(chars)); + final ByteBuffer encoded = Charset.forName("UTF-16LE").encode(CharBuffer.wrap(chars)); if (encoded.hasArray()) { return encoded.array(); } - byte[] e = new byte[encoded.remaining()]; + final byte[] e = new byte[encoded.remaining()]; encoded.get(e); return e; } + + private static int assertFitsIntoNonNegativeInt(final String what, final long value) + throws IOException { + if (value > Integer.MAX_VALUE || value < 0) { + throw new IOException("Cannot handle " + what + " " + value); + } + return (int) value; + } + + private static class ArchiveStatistics { + private int numberOfPackedStreams; + private long numberOfCoders; + private long numberOfOutStreams; + private long numberOfInStreams; + private long numberOfUnpackSubStreams; + private int numberOfFolders; + private BitSet folderHasCrc; + private int numberOfEntries; + private int numberOfEntriesWithStream; + + @Override + public String toString() { + return "Archive with " + + numberOfEntries + + " entries in " + + numberOfFolders + + " folders. Estimated size " + + estimateSize() / 1024L + + " kB."; + } + + long estimateSize() { + final long lowerBound = + 16L * numberOfPackedStreams /* packSizes, packCrcs in Archive */ + + numberOfPackedStreams / 8 /* packCrcsDefined in Archive */ + + numberOfFolders * folderSize() /* folders in Archive */ + + numberOfCoders * coderSize() /* coders in Folder */ + + (numberOfOutStreams - numberOfFolders) * bindPairSize() /* bindPairs in Folder */ + + 8L + * (numberOfInStreams + - numberOfOutStreams + + numberOfFolders) /* packedStreams in Folder */ + + 8L * numberOfOutStreams /* unpackSizes in Folder */ + + numberOfEntries * entrySize() /* files in Archive */ + + streamMapSize(); + return 2 * lowerBound /* conservative guess */; + } + + void assertValidity(final int maxMemoryLimitInKb) throws IOException { + if (numberOfEntriesWithStream > 0 && numberOfFolders == 0) { + throw new IOException("archive with entries but no folders"); + } + if (numberOfEntriesWithStream > numberOfUnpackSubStreams) { + throw new IOException("archive doesn't contain enough substreams for entries"); + } + + final long memoryNeededInKb = estimateSize() / 1024; + if (maxMemoryLimitInKb < memoryNeededInKb) { + throw new MemoryLimitException(memoryNeededInKb, maxMemoryLimitInKb); + } + } + + private long folderSize() { + return 30; /* nested arrays are accounted for separately */ + } + + private long coderSize() { + return 2 /* methodId is between 1 and four bytes currently, COPY and LZMA2 are the most common with 1 */ + + 16 + + 4 /* properties, guess */; + } + + private long bindPairSize() { + return 16; + } + + private long entrySize() { + return 100; /* real size depends on name length, everything without name is about 70 bytes */ + } + + private long streamMapSize() { + return 8 * numberOfFolders /* folderFirstPackStreamIndex, folderFirstFileIndex */ + + 8 * numberOfPackedStreams /* packStreamOffsets */ + + 4 * numberOfEntries /* fileFolderIndex */; + } + } } diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZFileOptions.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZFileOptions.java new file mode 100644 index 0000000000..77df73350a --- /dev/null +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZFileOptions.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2014-2022 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.filesystem.compressed.sevenz; + +/** + * Collects options for reading 7z archives. + * + * @since 1.19 @Immutable + */ +public class SevenZFileOptions { + private static final int DEFAUL_MEMORY_LIMIT_IN_KB = Integer.MAX_VALUE; + private static final boolean DEFAULT_USE_DEFAULTNAME_FOR_UNNAMED_ENTRIES = false; + private static final boolean DEFAULT_TRY_TO_RECOVER_BROKEN_ARCHIVES = false; + + private final int maxMemoryLimitInKb; + private final boolean useDefaultNameForUnnamedEntries; + private final boolean tryToRecoverBrokenArchives; + + private SevenZFileOptions( + final int maxMemoryLimitInKb, + final boolean useDefaultNameForUnnamedEntries, + final boolean tryToRecoverBrokenArchives) { + this.maxMemoryLimitInKb = maxMemoryLimitInKb; + this.useDefaultNameForUnnamedEntries = useDefaultNameForUnnamedEntries; + this.tryToRecoverBrokenArchives = tryToRecoverBrokenArchives; + } + + /** + * The default options. + * + *

    + *
  • no memory limit + *
  • don't modify the name of unnamed entries + *
+ */ + public static final SevenZFileOptions DEFAULT = + new SevenZFileOptions( + DEFAUL_MEMORY_LIMIT_IN_KB, + DEFAULT_USE_DEFAULTNAME_FOR_UNNAMED_ENTRIES, + DEFAULT_TRY_TO_RECOVER_BROKEN_ARCHIVES); + + /** + * Obtains a builder for SevenZFileOptions. + * + * @return a builder for SevenZFileOptions. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the maximum amount of memory to use for parsing the archive and during extraction. + * + *

Not all codecs will honor this setting. Currently only lzma and lzma2 are supported. + * + * @return the maximum amount of memory to use for extraction + */ + public int getMaxMemoryLimitInKb() { + return maxMemoryLimitInKb; + } + + /** + * Gets whether entries without a name should get their names set to the archive's default file + * name. + * + * @return whether entries without a name should get their names set to the archive's default file + * name + */ + public boolean getUseDefaultNameForUnnamedEntries() { + return useDefaultNameForUnnamedEntries; + } + + /** + * Whether {@link SevenZFile} shall try to recover from a certain type of broken archive. + * + * @return whether SevenZFile shall try to recover from a certain type of broken archive. + * @since 1.21 + */ + public boolean getTryToRecoverBrokenArchives() { + return tryToRecoverBrokenArchives; + } + + /** + * Mutable builder for the immutable {@link SevenZFileOptions}. + * + * @since 1.19 + */ + public static class Builder { + private int maxMemoryLimitInKb = DEFAUL_MEMORY_LIMIT_IN_KB; + private boolean useDefaultNameForUnnamedEntries = DEFAULT_USE_DEFAULTNAME_FOR_UNNAMED_ENTRIES; + private boolean tryToRecoverBrokenArchives = DEFAULT_TRY_TO_RECOVER_BROKEN_ARCHIVES; + + /** + * Sets the maximum amount of memory to use for parsing the archive and during extraction. + * + *

Not all codecs will honor this setting. Currently only lzma and lzma2 are supported. + * + * @param maxMemoryLimitInKb limit of the maximum amount of memory to use + * @return the reconfigured builder + */ + public Builder withMaxMemoryLimitInKb(final int maxMemoryLimitInKb) { + this.maxMemoryLimitInKb = maxMemoryLimitInKb; + return this; + } + + /** + * Sets whether entries without a name should get their names set to the archive's default file + * name. + * + * @param useDefaultNameForUnnamedEntries if true the name of unnamed entries will be set to the + * archive's default name + * @return the reconfigured builder + */ + public Builder withUseDefaultNameForUnnamedEntries( + final boolean useDefaultNameForUnnamedEntries) { + this.useDefaultNameForUnnamedEntries = useDefaultNameForUnnamedEntries; + return this; + } + + /** + * Sets whether {@link SevenZFile} will try to revover broken archives where the CRC of the + * file's metadata is 0. + * + *

This special kind of broken archive is encountered when mutli volume archives are closed + * prematurely. If you enable this option SevenZFile will trust data that looks as if it could + * contain metadata of an archive and allocate big amounts of memory. It is strongly recommended + * to not enable this option without setting {@link #withMaxMemoryLimitInKb} at the same time. + * + * @param tryToRecoverBrokenArchives if true SevenZFile will try to recover archives that are + * broken in the specific way + * @return the reconfigured builder + * @since 1.21 + */ + public Builder withTryToRecoverBrokenArchives(final boolean tryToRecoverBrokenArchives) { + this.tryToRecoverBrokenArchives = tryToRecoverBrokenArchives; + return this; + } + + /** + * Create the {@link SevenZFileOptions}. + * + * @return configured {@link SevenZFileOptions}. + */ + public SevenZFileOptions build() { + return new SevenZFileOptions( + maxMemoryLimitInKb, useDefaultNameForUnnamedEntries, tryToRecoverBrokenArchives); + } + } +} diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZMethod.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZMethod.java index 4709caff5c..aa5da65a6e 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZMethod.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZMethod.java @@ -106,8 +106,9 @@ public enum SevenZMethod { } byte[] getId() { - final byte[] copy = new byte[id.length]; - System.arraycopy(id, 0, copy, 0, id.length); + final int idLength = id.length; + final byte[] copy = new byte[idLength]; + System.arraycopy(id, 0, copy, 0, idLength); return copy; } diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZMethodConfiguration.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZMethodConfiguration.java index 1145ddc600..1db39d9b35 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZMethodConfiguration.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZMethodConfiguration.java @@ -20,13 +20,16 @@ package com.amaze.filemanager.filesystem.compressed.sevenz; +import java.util.Objects; + /** * Combines a SevenZMethod with configuration options for the method. * *

The exact type and interpretation of options depends on the method being configured. Currently * supported are: * - * + *
+ * * * * @@ -85,4 +88,21 @@ public SevenZMethod getMethod() { public Object getOptions() { return options; } + + @Override + public int hashCode() { + return method == null ? 0 : method.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final SevenZMethodConfiguration other = (SevenZMethodConfiguration) obj; + return Objects.equals(method, other.method) && Objects.equals(options, other.options); + } } diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZOutputFile.java b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZOutputFile.java index 685aa0bc7c..13b3017c29 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZOutputFile.java +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/SevenZOutputFile.java @@ -26,6 +26,7 @@ import java.io.DataOutputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; @@ -51,11 +52,11 @@ public class SevenZOutputFile implements Closeable { private final RandomAccessFile channel; private final List files = new ArrayList<>(); - private int numNonEmptyStreams = 0; + private int numNonEmptyStreams; private final CRC32 crc32 = new CRC32(); private final CRC32 compressedCrc32 = new CRC32(); - private long fileBytesWritten = 0; - private boolean finished = false; + private long fileBytesWritten; + private boolean finished; private CountingOutputStream currentOutputStream; private CountingOutputStream[] additionalCountingStreams; private Iterable contentMethods = @@ -65,11 +66,11 @@ public class SevenZOutputFile implements Closeable { /** * Opens file to write a 7z archive to. * - * @param filename the file to write to + * @param fileName the file to write to * @throws IOException if opening the file fails */ - public SevenZOutputFile(final File filename) throws IOException { - this(new RandomAccessFile(filename, "")); + public SevenZOutputFile(final File fileName) throws IOException { + this(new RandomAccessFile(fileName, "rwd")); } /** @@ -138,10 +139,8 @@ public void close() throws IOException { * @param inputFile file to create an entry from * @param entryName the name to use * @return the ArchiveEntry set up with details from the file - * @throws IOException on error */ - public SevenZArchiveEntry createArchiveEntry(final File inputFile, final String entryName) - throws IOException { + public SevenZArchiveEntry createArchiveEntry(final File inputFile, final String entryName) { final SevenZArchiveEntry entry = new SevenZArchiveEntry(); entry.setDirectory(inputFile.isDirectory()); entry.setName(entryName); @@ -156,9 +155,8 @@ public SevenZArchiveEntry createArchiveEntry(final File inputFile, final String * to complete the process. * * @param archiveEntry describes the entry - * @throws IOException on error */ - public void putArchiveEntry(final ArchiveEntry archiveEntry) throws IOException { + public void putArchiveEntry(final ArchiveEntry archiveEntry) { final SevenZArchiveEntry entry = (SevenZArchiveEntry) archiveEntry; files.add(entry); } @@ -237,6 +235,21 @@ public void write(final byte[] b, final int off, final int len) throws IOExcepti } } + /** + * Writes all of the given input stream to the current archive entry. + * + * @param inputStream the data source. + * @throws IOException if an I/O error occurs. + * @since 1.21 + */ + public void write(final InputStream inputStream) throws IOException { + final byte[] buffer = new byte[8024]; + int n = 0; + while (-1 != (n = inputStream.read(buffer))) { + write(buffer, 0, n); + } + } + /** * Finishes the addition of entries to this archive, without closing it. * @@ -261,7 +274,7 @@ public void finish() throws IOException { final CRC32 crc32 = new CRC32(); crc32.update(headerBytes); - ByteBuffer bb = + final ByteBuffer bb = ByteBuffer.allocate( SevenZFile.sevenZSignature.length + 2 /* version */ @@ -307,7 +320,8 @@ private CountingOutputStream setupFileOutputStream() throws IOException { throw new IllegalStateException("No current 7z entry"); } - OutputStream out = new OutputStreamWrapper(); + // doesn't need to be closed, just wraps the instance field channel + OutputStream out = new OutputStreamWrapper(); // NOSONAR final ArrayList moreStreams = new ArrayList<>(); boolean first = true; for (final SevenZMethodConfiguration m : getContentMethods(files.get(files.size() - 1))) { @@ -320,7 +334,7 @@ private CountingOutputStream setupFileOutputStream() throws IOException { first = false; } if (!moreStreams.isEmpty()) { - additionalCountingStreams = moreStreams.toArray(new CountingOutputStream[moreStreams.size()]); + additionalCountingStreams = moreStreams.toArray(new CountingOutputStream[0]); } return new CountingOutputStream(out) { @Override diff --git a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/package.html b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/package.html index 975703b385..c5756f2336 100644 --- a/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/package.html +++ b/commons_compress_7z/src/main/java/com/amaze/filemanager/filesystem/compressed/sevenz/package.html @@ -1,4 +1,5 @@ - + + + + 7z package +

Provides classes for reading and writing archives using the 7z format.

From a645ca2f4be8fd63ba4abb697b5fe6feb26848e3 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 31 Dec 2022 19:07:45 +0530 Subject: [PATCH 037/384] rename `BackupPrefsFragment#putBoolean` to `BackupPrefsFragment#storePreference` :see_no_evil: --- .../ui/fragments/preferencefragments/BackupPrefsFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index 9f74d9a963..a6030391b3 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -148,7 +148,7 @@ class BackupPrefsFragment : BasePrefsFragment() { PreferenceManager.getDefaultSharedPreferences(requireActivity()).edit() for ((key, value) in map) - putBoolean(editor, key, value) + storePreference(editor, key, value) editor?.apply() @@ -178,7 +178,7 @@ class BackupPrefsFragment : BasePrefsFragment() { } @Suppress("UNCHECKED_CAST") - private fun putBoolean(editor: SharedPreferences.Editor?, key: String?, value: Any) { + private fun storePreference(editor: SharedPreferences.Editor?, key: String?, value: Any) { try { when (value::class.simpleName) { "Boolean" -> editor?.putBoolean(key, value as Boolean) From 47c065d0f01b0522bbd17513cfa65ada3b8afe3d Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 31 Dec 2022 19:10:45 +0530 Subject: [PATCH 038/384] use slf4j logger instead of `e.printstacktrace()` --- .../fragments/preferencefragments/BackupPrefsFragment.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt index a6030391b3..e727e1d922 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BackupPrefsFragment.kt @@ -36,11 +36,14 @@ import com.amaze.filemanager.TagsHelper import com.amaze.filemanager.ui.activities.MainActivity import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import org.slf4j.Logger +import org.slf4j.LoggerFactory import java.io.* class BackupPrefsFragment : BasePrefsFragment() { private val TAG: String = TagsHelper.getTag(BasePrefsFragment::class.java) + private val log: Logger = LoggerFactory.getLogger(BackupPrefsFragment::class.java) companion object { val IMPORT_BACKUP_FILE: Int = 2 @@ -83,7 +86,7 @@ class BackupPrefsFragment : BasePrefsFragment() { startActivity(intent) } catch (e: IOException) { Toast.makeText(context, getString(R.string.exporting_failed), Toast.LENGTH_SHORT).show() - e.printStackTrace() + log.error(getString(R.string.exporting_failed), e) } } @@ -170,7 +173,7 @@ class BackupPrefsFragment : BasePrefsFragment() { getString(R.string.importing_failed), Toast.LENGTH_SHORT ).show() - e.printStackTrace() + log.error(getString(R.string.importing_failed), e) } } else { Toast.makeText(context, getString(R.string.unknown_error), Toast.LENGTH_SHORT).show() @@ -194,7 +197,7 @@ class BackupPrefsFragment : BasePrefsFragment() { "${getString(R.string.import_failed_for)} $key", Toast.LENGTH_SHORT ).show() - e.printStackTrace() + log.error("${getString(R.string.import_failed_for)} $key", e) } } From b6965d5d22568eec16362bb63795455760aa7534 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 7 Jan 2023 17:56:35 +0800 Subject: [PATCH 039/384] Make app minimal requirement to Android 4.4 Fixes #3189 --- app/build.gradle | 2 +- .../java/com/amaze/filemanager/application/AppConfigTest.java | 3 +-- .../asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt | 3 +-- .../filemanager/asynchronous/asynctasks/DbViewerTaskTest.java | 3 +-- .../compress/AbstractCompressedHelperCallableTest.kt | 3 +-- .../asynctasks/compress/CompressedHelperForBadArchiveTest.kt | 2 +- .../asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt | 3 +-- .../asynctasks/texteditor/read/ReadTextFileCallableTest.kt | 3 +-- .../texteditor/write/WriteTextFileCallableTest.java | 3 +-- .../filemanager/asynchronous/services/EncryptServiceTest.kt | 3 +-- .../amaze/filemanager/asynchronous/services/ZipServiceTest.kt | 4 +--- .../filemanager/database/ExplorerDatabaseMigrationTest.kt | 3 +-- .../filemanager/database/UtilitiesDatabaseMigrationTest.kt | 3 +-- .../java/com/amaze/filemanager/database/UtilsHandlerTest.kt | 3 +-- .../typeconverters/EncryptedStringTypeConverterTest.kt | 3 +-- .../filemanager/filesystem/AbstractOperationsTestBase.kt | 3 +-- .../filemanager/filesystem/EditableFileAbstractionTest.java | 3 +-- .../java/com/amaze/filemanager/filesystem/HybridFileTest.kt | 3 +-- .../java/com/amaze/filemanager/filesystem/OperationsTest.java | 3 +-- .../java/com/amaze/filemanager/filesystem/RootHelperTest.java | 3 +-- .../filemanager/filesystem/compressed/B0rkenZipTest.java | 3 +-- .../filesystem/compressed/CompressedHelperTest.java | 3 +-- .../compressed/extractcontents/AbstractExtractorTest.kt | 3 +-- .../filemanager/filesystem/files/FileListSorterTest.java | 3 +-- .../com/amaze/filemanager/filesystem/files/FileUtilsTest.kt | 3 +-- .../filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt | 3 +-- .../ftpserver/commands/AbstractFtpserverCommandTest.kt | 3 +-- .../amaze/filemanager/filesystem/root/ListFilesCommandTest.kt | 3 +-- .../filemanager/filesystem/root/ListFilesCommandTest2.kt | 3 +-- .../amaze/filemanager/filesystem/smb/CifsContextsTest.java | 3 +-- .../com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt | 3 +-- .../filemanager/filesystem/ssh/AbstractSftpServerTest.java | 3 +-- .../filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt | 3 +-- .../com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt | 3 +-- .../com/amaze/filemanager/ui/activities/MainActivityTest.java | 3 +-- .../filemanager/ui/activities/TextEditorActivityTest.java | 3 +-- .../filemanager/ui/dialogs/AbstractEncryptDialogTests.kt | 3 +-- .../filemanager/ui/fragments/CloudSheetFragmentTest.java | 3 +-- .../test/java/com/amaze/filemanager/ui/icons/IconsTest.java | 3 +-- .../test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt | 3 +-- .../filemanager/ui/views/WarnableTextInputValidatorTest.java | 3 +-- .../test/java/com/amaze/filemanager/utils/CryptUtilTest.kt | 3 +-- .../java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt | 3 +-- app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt | 3 +-- app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java | 3 +-- app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java | 3 +-- .../com/amaze/filemanager/utils/X509CertificateUtilTest.kt | 3 +-- .../compressed/extractcontents/MultipartRarExtractorTest.kt | 2 +- .../amaze/filemanager/utils/PackageInstallValidationTest.kt | 3 +-- .../filesystem/cloud/CloudStreamSourceTest.java | 3 +-- .../filesystem/smbstreamer/StreamSourceTest.java | 3 +-- .../com/amaze/filemanager/test/ShadowPasswordUtilTest.java | 3 +-- 52 files changed, 52 insertions(+), 102 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6334fe80cc..cfc5ccdc71 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { defaultConfig { applicationId "com.amaze.filemanager" - minSdkVersion 14 + minSdkVersion 19 targetSdkVersion 31 versionCode 117 versionName "3.8.4" diff --git a/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java b/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java index b623cc39df..7d4f53312e 100644 --- a/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java +++ b/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.application; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static android.os.Looper.getMainLooper; @@ -51,7 +50,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {JELLY_BEAN, KITKAT, P}) +@Config(sdk = {KITKAT, P}) public class AppConfigTest { @After diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt index 32a755dff1..ee2be561bc 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt @@ -22,7 +22,6 @@ package com.amaze.filemanager.asynchronous.asynctasks import android.content.Context import android.os.Build -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Looper @@ -61,7 +60,7 @@ import org.robolectric.shadows.ShadowToast ShadowTabHandler::class, ShadowPasswordUtil::class ], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) abstract class AbstractDeleteTaskTestBase { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java index 811533fc6a..3ee43925d9 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.asynchronous.asynctasks; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static android.os.Looper.getMainLooper; @@ -58,7 +57,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class DbViewerTaskTest { private WebView webView; diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt index 4c24574f56..e66761685e 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.asynchronous.asynctasks.compress -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -37,7 +36,7 @@ import java.io.FileOutputStream import java.util.* @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [JELLY_BEAN, KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) abstract class AbstractCompressedHelperCallableTest { private lateinit var systemTz: TimeZone diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/CompressedHelperForBadArchiveTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/CompressedHelperForBadArchiveTest.kt index c213f7f31d..908f1cc79f 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/CompressedHelperForBadArchiveTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/CompressedHelperForBadArchiveTest.kt @@ -41,7 +41,7 @@ import java.io.FileOutputStream * Test behaviour of CompressedHelpers in handling corrupt archives. */ @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [JELLY_BEAN, KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) class CompressedHelperForBadArchiveTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt index f5257d2e34..ac450a557a 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt @@ -21,7 +21,6 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh import android.content.Context -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -61,7 +60,7 @@ import java.net.SocketException import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [JELLY_BEAN, KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) @Suppress("StringLiteralDuplication") class SshAuthenticationTaskTest { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt index 78b2790f75..f6e3cd814b 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt @@ -22,7 +22,6 @@ package com.amaze.filemanager.asynchronous.asynctasks.texteditor.read import android.content.Context import android.net.Uri -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -46,7 +45,7 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class ReadTextFileCallableTest { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java index d9b2f1430a..93b7e20fee 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.asynchronous.asynctasks.texteditor.write; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; @@ -65,7 +64,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class, ShadowContentResolver.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class WriteTextFileCallableTest { private static final String contents = "This is modified data"; diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt index 99566ba153..8d7d34938d 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt @@ -24,7 +24,6 @@ import android.app.NotificationManager import android.content.Context import android.content.Intent import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.M import android.os.Build.VERSION_CODES.P @@ -68,7 +67,7 @@ import java.util.concurrent.TimeUnit import kotlin.random.Random @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [JELLY_BEAN, KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) class EncryptServiceTest { private lateinit var service: EncryptService diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt index 73d5643a6f..7127f13ff6 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt @@ -22,7 +22,6 @@ package com.amaze.filemanager.asynchronous.services import android.content.Context import android.content.Intent -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Looper.getMainLooper @@ -53,12 +52,11 @@ import java.time.ZoneId.of import java.time.format.DateTimeFormatter import java.util.* import java.util.concurrent.TimeUnit -import kotlin.collections.ArrayList import kotlin.random.Random @RunWith(RobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) -@Config(shadows = [ShadowMultiDex::class], sdk = [JELLY_BEAN, KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) class ZipServiceTest { val dt = DateTimeFormatter.ofPattern("yyyyMMddkkmm") diff --git a/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt b/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt index dd7a97b338..b5ab096ba3 100644 --- a/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.database -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.room.Room @@ -44,7 +43,7 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) @Suppress("StringLiteralDuplication", "ComplexMethod", "LongMethod") class ExplorerDatabaseMigrationTest { diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt index 5d529b7ab3..e482ab901c 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.database -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.util.Base64 @@ -54,7 +53,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class UtilitiesDatabaseMigrationTest { diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt index 52be6456df..fe566db72e 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.database -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -53,7 +52,7 @@ import java.io.File @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class UtilsHandlerTest { diff --git a/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt b/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt index 27b54d2aad..fa63d9c2ba 100644 --- a/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.database.typeconverters -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -38,7 +37,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class EncryptedStringTypeConverterTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt index a0d6093001..76e47ceff9 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt @@ -22,7 +22,6 @@ package com.amaze.filemanager.filesystem import android.content.Context import android.os.Build -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Looper @@ -61,7 +60,7 @@ import org.robolectric.shadows.ShadowSQLiteConnection ShadowTabHandler::class, ShadowPasswordUtil::class ], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) abstract class AbstractOperationsTestBase { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java index 352bb5a0f7..51bf089efa 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static com.amaze.filemanager.filesystem.EditableFileAbstraction.Scheme.CONTENT; @@ -49,7 +48,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class EditableFileAbstractionTest { @Test(expected = IllegalArgumentException.class) diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt index 4d2daafd78..ce7829f160 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -39,7 +38,7 @@ import kotlin.random.Random /* ktlint-disable max-line-length */ @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [JELLY_BEAN, KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) class HybridFileTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java index 84da62de0c..a37422200d 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertFalse; @@ -45,7 +44,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class OperationsTest { private File storageRoot = Environment.getExternalStorageDirectory(); diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java index f576ed94e8..dd87605eb5 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.fail; @@ -51,7 +50,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) @Ignore("FIXME: should not ignore - please implement a shadow") public class RootHelperTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java index dcb7b61e66..26483a4475 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem.compressed; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static kotlin.io.ConstantsKt.DEFAULT_BUFFER_SIZE; @@ -56,7 +55,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class B0rkenZipTest { private File zipfile1 = new File(Environment.getExternalStorageDirectory(), "zip-slip.zip"); diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java index 5612a99c4c..bffc1b832f 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem.compressed; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; @@ -66,7 +65,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class CompressedHelperTest { private Context context; diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt index fd129da301..c5aea2b62f 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt @@ -21,7 +21,6 @@ package com.amaze.filemanager.filesystem.compressed.extractcontents import android.content.Context -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -45,7 +44,7 @@ import java.nio.file.Paths import java.util.* @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [JELLY_BEAN, KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) abstract class AbstractExtractorTest { protected abstract fun extractorClass(): Class diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.java index 79cc0edc1c..b2c89ab3de 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem.files; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.hamcrest.Matchers.greaterThan; @@ -46,7 +45,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class FileListSorterTest { /** * Purpose: when dirsOnTop is 0, if file1 is directory && file2 is not directory, result is -1 diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt index 80085c4017..10fddf8378 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem.files -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -36,7 +35,7 @@ import java.util.* @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) -@Config(sdk = [JELLY_BEAN, KITKAT, P]) +@Config(sdk = [KITKAT, P]) @Suppress("TooManyFunctions", "StringLiteralDuplication") class FileUtilsTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt index 2103f4c842..7160b58ea3 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem.ftp -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -61,7 +60,7 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class NetCopyClientConnectionPoolFtpTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt index e3dcd089f8..67101cfc40 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem.ftpserver.commands -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -38,7 +37,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) abstract class AbstractFtpserverCommandTest { 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 e922d54ba0..6db5b63adb 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 @@ -21,7 +21,6 @@ package com.amaze.filemanager.filesystem.root import android.content.SharedPreferences -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.preference.PreferenceManager @@ -54,7 +53,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class ListFilesCommandTest { 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 da9ad65a4e..eb4175459d 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 @@ -21,7 +21,6 @@ package com.amaze.filemanager.filesystem.root import android.content.SharedPreferences -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.preference.PreferenceManager @@ -59,7 +58,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class ListFilesCommandTest2 { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java index 00ef3f47af..cbd28cc06d 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem.smb; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; @@ -43,7 +42,7 @@ import jcifs.ResolverType; import jcifs.context.BaseContext; -@Config(sdk = {JELLY_BEAN, KITKAT, P}) +@Config(sdk = {KITKAT, P}) @RunWith(AndroidJUnit4.class) public class CifsContextsTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt index e21fbd3536..8a4be016fa 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt @@ -21,7 +21,6 @@ package com.amaze.filemanager.filesystem.smb import android.content.Context -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -45,7 +44,7 @@ import org.robolectric.shadows.ShadowSQLiteConnection @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowSmbUtil::class, ShadowMultiDex::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) @LooperMode(LooperMode.Mode.PAUSED) class SmbHybridFileTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java index 71af20c968..1a30671ef4 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem.ssh; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX; @@ -58,7 +57,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public abstract class AbstractSftpServerTest { protected SshServer server; diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt index f65799ae00..687d29ebc6 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.filesystem.ssh -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -70,7 +69,7 @@ import java.security.KeyPair @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class NetCopyClientConnectionPoolSshTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt index 68bb7cf910..cb85826740 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt @@ -21,7 +21,6 @@ package com.amaze.filemanager.filesystem.ssh import android.content.Context -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -46,7 +45,7 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class SshHybridFileTest { 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 c2dc76569c..3b9ce346ac 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 @@ -20,7 +20,6 @@ package com.amaze.filemanager.ui.activities; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.P; @@ -79,7 +78,7 @@ @RunWith(AndroidJUnit4.class) @Config( - sdk = {JELLY_BEAN, KITKAT, P}, + sdk = {KITKAT, P}, shadows = { ShadowMultiDex.class, ShadowStorageManager.class, diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java b/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java index ddc5e65526..0ba4a68d69 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.ui.activities; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; @@ -58,7 +57,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class TextEditorActivityTest { private final String fileContents = "fsdfsdfs"; diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt index 99dd8f1102..13cf133e23 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt @@ -21,7 +21,6 @@ package com.amaze.filemanager.ui.dialogs import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.N import android.os.Build.VERSION_CODES.P @@ -41,7 +40,7 @@ import org.robolectric.annotation.Config * Base class for various tests related to file encryption. */ @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class, ShadowTabHandler::class], sdk = [JELLY_BEAN, KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class, ShadowTabHandler::class], sdk = [KITKAT, P]) abstract class AbstractEncryptDialogTests { protected lateinit var scenario: ActivityScenario diff --git a/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java b/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java index 7e066a8620..7e2018a67c 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.ui.fragments; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertFalse; @@ -39,7 +38,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {JELLY_BEAN, KITKAT, P}) +@Config(sdk = {KITKAT, P}) public class CloudSheetFragmentTest { @Test diff --git a/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java b/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java index 7042034aa6..f8873caf22 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.ui.icons; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; @@ -41,7 +40,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class IconsTest { @Before diff --git a/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt b/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt index 19be9db0f4..8e49c7e214 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt @@ -22,7 +22,6 @@ package com.amaze.filemanager.ui.theme import android.content.Context import android.content.res.Configuration -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -37,7 +36,7 @@ import java.util.* @RunWith(AndroidJUnit4::class) @Config( - sdk = [JELLY_BEAN, KITKAT, P], + sdk = [KITKAT, P], shadows = [ShadowMultiDex::class] ) class AppThemeTest { diff --git a/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java b/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java index c2b8ec3d23..032526308b 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.ui.views; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; @@ -46,7 +45,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {JELLY_BEAN, KITKAT, P}) +@Config(sdk = {KITKAT, P}) public class WarnableTextInputValidatorTest { private Context context; diff --git a/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt index e9c376a570..e0494d24de 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.utils -import android.os.Build.VERSION_CODES.JELLY_BEAN_MR2 import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -48,7 +47,7 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) @Config( - sdk = [JELLY_BEAN_MR2, KITKAT, P] + sdk = [KITKAT, P] ) class CryptUtilTest { diff --git a/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt b/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt index 6c5df77c95..10017c5985 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.utils -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.text.SpannedString @@ -32,7 +31,7 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) -@Config(sdk = [JELLY_BEAN, KITKAT, P]) +@Config(sdk = [KITKAT, P]) class MinMaxInputFilterTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt index 03f8dda12b..df499decb9 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.utils -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -47,7 +46,7 @@ import org.robolectric.annotation.Config @Suppress("StringLiteralDuplication") @RunWith(AndroidJUnit4::class) @Config( - sdk = [JELLY_BEAN, KITKAT, P], + sdk = [KITKAT, P], shadows = [ShadowPasswordUtil::class, ShadowSmbUtil::class] ) class SmbUtilTest { diff --git a/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java b/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java index c0bb401d98..db778827d3 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.utils; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertArrayEquals; @@ -38,7 +37,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {JELLY_BEAN, KITKAT, P}) +@Config(sdk = {KITKAT, P}) public class TinyDBTest { private SharedPreferences prefs; diff --git a/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java b/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java index ed39e757a1..a53083910f 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.utils; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.P; @@ -57,7 +56,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {JELLY_BEAN, KITKAT, P}) +@Config(sdk = {KITKAT, P}) public class UtilsTest { @Test diff --git a/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt index c31cf66ea5..3b4aee022e 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt @@ -20,7 +20,6 @@ package com.amaze.filemanager.utils -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -43,7 +42,7 @@ import javax.security.cert.X509Certificate @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P] ) class X509CertificateUtilTest { diff --git a/app/src/testPlay/java/com/amaze/filemanager/filesystem/compressed/extractcontents/MultipartRarExtractorTest.kt b/app/src/testPlay/java/com/amaze/filemanager/filesystem/compressed/extractcontents/MultipartRarExtractorTest.kt index 00ee84c522..e087520174 100644 --- a/app/src/testPlay/java/com/amaze/filemanager/filesystem/compressed/extractcontents/MultipartRarExtractorTest.kt +++ b/app/src/testPlay/java/com/amaze/filemanager/filesystem/compressed/extractcontents/MultipartRarExtractorTest.kt @@ -44,7 +44,7 @@ import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [Build.VERSION_CODES.JELLY_BEAN, Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.P] + sdk = [Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.P] ) class MultipartRarExtractorTest { diff --git a/app/src/testPlayRelease/java/com/amaze/filemanager/utils/PackageInstallValidationTest.kt b/app/src/testPlayRelease/java/com/amaze/filemanager/utils/PackageInstallValidationTest.kt index 5bdd17ab76..3f69dd66b1 100644 --- a/app/src/testPlayRelease/java/com/amaze/filemanager/utils/PackageInstallValidationTest.kt +++ b/app/src/testPlayRelease/java/com/amaze/filemanager/utils/PackageInstallValidationTest.kt @@ -24,7 +24,6 @@ import android.annotation.SuppressLint import android.content.Context import android.content.pm.PackageInfo import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.N import android.os.Build.VERSION_CODES.P @@ -66,7 +65,7 @@ import java.util.concurrent.TimeUnit @SuppressLint("SdCardPath") @RunWith(AndroidJUnit4::class) @Config( - sdk = [JELLY_BEAN, KITKAT, P], + sdk = [KITKAT, P], shadows = [ShadowPackageManager::class, ShadowMultiDex::class, ShadowTabHandler::class] ) class PackageInstallValidationTest { diff --git a/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/cloud/CloudStreamSourceTest.java b/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/cloud/CloudStreamSourceTest.java index d42ddfbf21..d0e87e353f 100644 --- a/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/cloud/CloudStreamSourceTest.java +++ b/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/cloud/CloudStreamSourceTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.fileoperations.filesystem.cloud; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertArrayEquals; @@ -51,7 +50,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class CloudStreamSourceTest { private CloudStreamSource cs; private String testFilePath; diff --git a/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/smbstreamer/StreamSourceTest.java b/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/smbstreamer/StreamSourceTest.java index e89e5d0115..c8b012fa04 100644 --- a/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/smbstreamer/StreamSourceTest.java +++ b/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/smbstreamer/StreamSourceTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.fileoperations.filesystem.smbstreamer; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertArrayEquals; @@ -52,7 +51,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class, ShadowSmbFile.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class StreamSourceTest { private SmbFile file; private StreamSource ss; diff --git a/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java b/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java index 2ce8dcbd6e..12e9be1360 100644 --- a/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java +++ b/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.test; -import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static org.awaitility.Awaitility.await; @@ -56,7 +55,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class, ShadowPasswordUtil.class}, - sdk = {JELLY_BEAN, KITKAT, P}) + sdk = {KITKAT, P}) public class ShadowPasswordUtilTest { @Before From bf147c8a99956560407dbb9d3513fcd227358b9b Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Mon, 9 Jan 2023 22:34:11 +0800 Subject: [PATCH 040/384] Unit test adjustments - Robolectric Test against Android 4.4, 8.0 and 11.0 - Add GrantPermissionRule to unit tests that involve MainActivity, to grant MANAGE_EXTERNAL_STORAGE before starting --- .../application/AppConfigTest.java | 3 +- .../asynctasks/AbstractDeleteTaskTestBase.kt | 12 +- .../asynctasks/DbViewerTaskTest.java | 3 +- .../AbstractCompressedHelperCallableTest.kt | 3 +- .../ssh/SshAuthenticationTaskTest.kt | 3 +- .../read/ReadTextFileCallableTest.kt | 3 +- .../write/WriteTextFileCallableTest.java | 3 +- .../services/DecryptServiceTest.kt | 3 +- .../services/EncryptServiceTest.kt | 3 +- .../services/ExtractServiceTest.kt | 11 ++ .../asynchronous/services/ZipServiceTest.kt | 3 +- .../database/ExplorerDatabaseMigrationTest.kt | 3 +- .../UtilitiesDatabaseMigrationTest.kt | 3 +- .../filemanager/database/UtilsHandlerTest.kt | 3 +- .../EncryptedStringTypeConverterTest.kt | 3 +- .../filesystem/AbstractOperationsTestBase.kt | 16 +- .../EditableFileAbstractionTest.java | 3 +- .../filemanager/filesystem/HybridFileTest.kt | 3 +- .../filesystem/OperationsTest.java | 3 +- .../filesystem/RootHelperTest.java | 3 +- .../filesystem/compressed/B0rkenZipTest.java | 3 +- .../compressed/CompressedHelperTest.java | 3 +- .../extractcontents/AbstractExtractorTest.kt | 3 +- .../filesystem/files/FileListSorterTest.java | 4 +- .../filesystem/files/FileUtilsTest.kt | 3 +- .../ftp/NetCopyClientConnectionPoolFtpTest.kt | 3 +- .../commands/AbstractFtpserverCommandTest.kt | 3 +- .../filesystem/root/ListFilesCommandTest.kt | 3 +- .../filesystem/root/ListFilesCommandTest2.kt | 3 +- .../filesystem/smb/CifsContextsTest.java | 4 +- .../filesystem/smb/SmbHybridFileTest.kt | 3 +- .../ssh/AbstractSftpServerTest.java | 3 +- .../ssh/NetCopyClientConnectionPoolSshTest.kt | 3 +- .../filesystem/ssh/SshHybridFileTest.kt | 3 +- .../ui/activities/MainActivityTest.java | 11 +- .../ui/activities/PermissionsActivityTest.kt | 155 ++++++++++++++++++ .../ui/activities/TextEditorActivityTest.java | 3 +- .../ui/dialogs/AbstractEncryptDialogTests.kt | 16 +- .../ui/fragments/CloudSheetFragmentTest.java | 3 +- .../amaze/filemanager/ui/icons/IconsTest.java | 3 +- .../NotificationConstantsTest.java | 2 +- .../filemanager/ui/theme/AppThemeTest.kt | 3 +- .../views/WarnableTextInputValidatorTest.java | 3 +- .../amaze/filemanager/utils/AESCryptTest.kt | 3 +- .../filemanager/utils/AnimUtilsTest.java | 3 +- .../amaze/filemanager/utils/CryptUtilTest.kt | 3 +- .../utils/MinMaxInputFilterTest.kt | 3 +- .../amaze/filemanager/utils/SmbUtilTest.kt | 3 +- .../amaze/filemanager/utils/TinyDBTest.java | 3 +- .../amaze/filemanager/utils/UtilsTest.java | 2 +- .../utils/X509CertificateUtilTest.kt | 3 +- .../test/ShadowPasswordUtilTest.java | 3 +- .../com/amaze/filemanager/test/TestUtils.kt | 11 ++ 53 files changed, 318 insertions(+), 52 deletions(-) create mode 100644 app/src/test/java/com/amaze/filemanager/ui/activities/PermissionsActivityTest.kt diff --git a/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java b/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java index 7d4f53312e..b92d24725a 100644 --- a/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java +++ b/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java @@ -44,13 +44,14 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.MemoryCategory; +import android.os.Build; import android.os.StrictMode; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {KITKAT, P}) +@Config(sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class AppConfigTest { @After diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt index ee2be561bc..bf85edb25a 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt @@ -20,16 +20,19 @@ package com.amaze.filemanager.asynchronous.asynctasks +import android.Manifest import android.content.Context import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Looper import android.os.storage.StorageManager +import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule import com.amaze.filemanager.R import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.shadows.ShadowMultiDex @@ -44,6 +47,7 @@ import io.reactivex.schedulers.Schedulers import org.junit.After import org.junit.Assert.* import org.junit.Before +import org.junit.Rule import org.junit.runner.RunWith import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @@ -60,12 +64,18 @@ import org.robolectric.shadows.ShadowToast ShadowTabHandler::class, ShadowPasswordUtil::class ], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) abstract class AbstractDeleteTaskTestBase { private var ctx: Context? = null + @Rule + @JvmField + @RequiresApi(Build.VERSION_CODES.R) + val allFilesPermissionRule = GrantPermissionRule + .grant(Manifest.permission.MANAGE_EXTERNAL_STORAGE) + /** * Test case setup. * diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java index 3ee43925d9..8e3e775138 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java @@ -47,6 +47,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.os.Build; import android.view.View; import android.webkit.WebView; import android.widget.TextView; @@ -57,7 +58,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class DbViewerTaskTest { private WebView webView; diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt index e66761685e..a9d4b780c2 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.compress +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -36,7 +37,7 @@ import java.io.FileOutputStream import java.util.* @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P, Build.VERSION_CODES.R]) abstract class AbstractCompressedHelperCallableTest { private lateinit var systemTz: TimeZone diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt index ac450a557a..491b7dd717 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt @@ -21,6 +21,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh import android.content.Context +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -60,7 +61,7 @@ import java.net.SocketException import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P, Build.VERSION_CODES.R]) @Suppress("StringLiteralDuplication") class SshAuthenticationTaskTest { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt index f6e3cd814b..b4805a0ce4 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt @@ -22,6 +22,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.texteditor.read import android.content.Context import android.net.Uri +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -45,7 +46,7 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class ReadTextFileCallableTest { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java index 93b7e20fee..ca7e6d6b6d 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java @@ -56,6 +56,7 @@ import android.content.ContentResolver; import android.content.Context; import android.net.Uri; +import android.os.Build; import android.os.Environment; import androidx.test.core.app.ApplicationProvider; @@ -64,7 +65,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class, ShadowContentResolver.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class WriteTextFileCallableTest { private static final String contents = "This is modified data"; diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/DecryptServiceTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/DecryptServiceTest.kt index 4dc503ed1d..cc3cca4bae 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/DecryptServiceTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/DecryptServiceTest.kt @@ -23,6 +23,7 @@ package com.amaze.filemanager.asynchronous.services import android.app.NotificationManager import android.content.Context import android.content.Intent +import android.os.Build import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.M @@ -63,7 +64,7 @@ import java.util.concurrent.TimeUnit import kotlin.random.Random @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P, Build.VERSION_CODES.R]) @Suppress("StringLiteralDuplication") class DecryptServiceTest { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt index 8d7d34938d..bbf62280f7 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt @@ -23,6 +23,7 @@ package com.amaze.filemanager.asynchronous.services import android.app.NotificationManager import android.content.Context import android.content.Intent +import android.os.Build import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.M @@ -67,7 +68,7 @@ import java.util.concurrent.TimeUnit import kotlin.random.Random @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P, Build.VERSION_CODES.R]) class EncryptServiceTest { private lateinit var service: EncryptService diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ExtractServiceTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ExtractServiceTest.kt index 5539306b07..8765894503 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ExtractServiceTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ExtractServiceTest.kt @@ -20,16 +20,20 @@ package com.amaze.filemanager.asynchronous.services +import android.Manifest import android.content.Context import android.content.Intent import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES.N import android.os.Build.VERSION_CODES.P import android.os.Environment +import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule import com.amaze.filemanager.BuildConfig import com.amaze.filemanager.R import com.amaze.filemanager.application.AppConfig @@ -47,6 +51,7 @@ import org.junit.Assert.assertNull import org.junit.Assert.fail import org.junit.Before import org.junit.Ignore +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Robolectric @@ -93,6 +98,12 @@ class ExtractServiceTest { private val multiVolumeRarFileV5Part2: File private val multiVolumeRarFileV5Part3: File + @Rule + @JvmField + @RequiresApi(VERSION_CODES.R) + val allFilesPermissionRule = GrantPermissionRule + .grant(Manifest.permission.MANAGE_EXTERNAL_STORAGE) + init { Environment.getExternalStorageDirectory().run { zipfile1 = File(this, "zip-slip.zip") diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt index 7127f13ff6..d77db736ca 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt @@ -22,6 +22,7 @@ package com.amaze.filemanager.asynchronous.services import android.content.Context import android.content.Intent +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Looper.getMainLooper @@ -56,7 +57,7 @@ import kotlin.random.Random @RunWith(RobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) -@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P, Build.VERSION_CODES.R]) class ZipServiceTest { val dt = DateTimeFormatter.ofPattern("yyyyMMddkkmm") diff --git a/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt b/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt index b5ab096ba3..37932fd892 100644 --- a/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.database +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.room.Room @@ -43,7 +44,7 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) @Suppress("StringLiteralDuplication", "ComplexMethod", "LongMethod") class ExplorerDatabaseMigrationTest { diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt index e482ab901c..e94a850483 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.database +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.util.Base64 @@ -53,7 +54,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class UtilitiesDatabaseMigrationTest { diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt index fe566db72e..e5bc68ad1f 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.database +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -52,7 +53,7 @@ import java.io.File @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class UtilsHandlerTest { diff --git a/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt b/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt index fa63d9c2ba..50019217b0 100644 --- a/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.database.typeconverters +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -37,7 +38,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class EncryptedStringTypeConverterTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt index 76e47ceff9..ff553eaec1 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt @@ -20,16 +20,19 @@ package com.amaze.filemanager.filesystem +import android.Manifest import android.content.Context import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Looper import android.os.storage.StorageManager +import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.shadows.ShadowSmbUtil @@ -43,6 +46,7 @@ import io.reactivex.schedulers.Schedulers import org.junit.After import org.junit.Assert.* import org.junit.Before +import org.junit.Rule import org.junit.runner.RunWith import org.robolectric.Shadows import org.robolectric.android.util.concurrent.InlineExecutorService @@ -60,13 +64,13 @@ import org.robolectric.shadows.ShadowSQLiteConnection ShadowTabHandler::class, ShadowPasswordUtil::class ], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) abstract class AbstractOperationsTestBase { - protected var ctx: Context? = null + private var ctx: Context? = null - protected val blankCallback = object : Operations.ErrorCallBack { + private val blankCallback = object : Operations.ErrorCallBack { override fun exists(file: HybridFile?) = Unit override fun launchSAF(file: HybridFile?) = Unit override fun launchSAF(file: HybridFile?, file1: HybridFile?) = Unit @@ -74,6 +78,12 @@ abstract class AbstractOperationsTestBase { override fun invalidName(file: HybridFile?) = Unit } + @Rule + @JvmField + @RequiresApi(Build.VERSION_CODES.R) + val allFilesPermissionRule = GrantPermissionRule + .grant(Manifest.permission.MANAGE_EXTERNAL_STORAGE) + /** * Test case setup. * diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java index 51bf089efa..eac28874d3 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java @@ -39,6 +39,7 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.net.Uri; +import android.os.Build; import android.os.Environment; import android.provider.OpenableColumns; @@ -48,7 +49,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class EditableFileAbstractionTest { @Test(expected = IllegalArgumentException.class) diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt index ce7829f160..4993edce5e 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.filesystem +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -38,7 +39,7 @@ import kotlin.random.Random /* ktlint-disable max-line-length */ @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P, Build.VERSION_CODES.R]) class HybridFileTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java index a37422200d..a5ddee4c1a 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java @@ -36,6 +36,7 @@ import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.shadows.ShadowMultiDex; +import android.os.Build; import android.os.Environment; import androidx.test.core.app.ApplicationProvider; @@ -44,7 +45,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class OperationsTest { private File storageRoot = Environment.getExternalStorageDirectory(); diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java index dd87605eb5..3e01e85d4a 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java @@ -43,6 +43,7 @@ import com.amaze.filemanager.filesystem.root.ListFilesCommand; import com.amaze.filemanager.shadows.ShadowMultiDex; +import android.os.Build; import android.os.Environment; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -50,7 +51,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) @Ignore("FIXME: should not ignore - please implement a shadow") public class RootHelperTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java index 26483a4475..99bb6b9944 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java @@ -45,6 +45,7 @@ import com.amaze.filemanager.filesystem.compressed.extractcontents.helpers.ZipExtractor; import com.amaze.filemanager.shadows.ShadowMultiDex; +import android.os.Build; import android.os.Environment; import androidx.test.core.app.ApplicationProvider; @@ -55,7 +56,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class B0rkenZipTest { private File zipfile1 = new File(Environment.getExternalStorageDirectory(), "zip-slip.zip"); diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java index bffc1b832f..85b15ab939 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java @@ -58,6 +58,7 @@ import com.amaze.filemanager.shadows.ShadowMultiDex; import android.content.Context; +import android.os.Build; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -65,7 +66,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class CompressedHelperTest { private Context context; diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt index c5aea2b62f..dbc2444af7 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt @@ -21,6 +21,7 @@ package com.amaze.filemanager.filesystem.compressed.extractcontents import android.content.Context +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -44,7 +45,7 @@ import java.nio.file.Paths import java.util.* @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P, Build.VERSION_CODES.R]) abstract class AbstractExtractorTest { protected abstract fun extractorClass(): Class diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.java index b2c89ab3de..35aa967cd1 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.java @@ -35,6 +35,8 @@ import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.shadows.ShadowMultiDex; +import android.os.Build; + import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -45,7 +47,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class FileListSorterTest { /** * Purpose: when dirsOnTop is 0, if file1 is directory && file2 is not directory, result is -1 diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt index 10fddf8378..5b1ba3133c 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.filesystem.files +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -35,7 +36,7 @@ import java.util.* @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) -@Config(sdk = [KITKAT, P]) +@Config(sdk = [KITKAT, P, Build.VERSION_CODES.R]) @Suppress("TooManyFunctions", "StringLiteralDuplication") class FileUtilsTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt index 7160b58ea3..2f3bc5223a 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.filesystem.ftp +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -60,7 +61,7 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class NetCopyClientConnectionPoolFtpTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt index 67101cfc40..6a9bbde6e9 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.filesystem.ftpserver.commands +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -37,7 +38,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) abstract class AbstractFtpserverCommandTest { 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 6db5b63adb..7f80ba0802 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 @@ -21,6 +21,7 @@ package com.amaze.filemanager.filesystem.root import android.content.SharedPreferences +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.preference.PreferenceManager @@ -53,7 +54,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class ListFilesCommandTest { 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 eb4175459d..5aa9403f76 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 @@ -21,6 +21,7 @@ package com.amaze.filemanager.filesystem.root import android.content.SharedPreferences +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.preference.PreferenceManager @@ -58,7 +59,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class ListFilesCommandTest2 { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java index cbd28cc06d..8695fdf445 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java @@ -36,13 +36,15 @@ import org.junit.runner.RunWith; import org.robolectric.annotation.Config; +import android.os.Build; + import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import jcifs.ResolverType; import jcifs.context.BaseContext; -@Config(sdk = {KITKAT, P}) +@Config(sdk = {KITKAT, P, Build.VERSION_CODES.R}) @RunWith(AndroidJUnit4.class) public class CifsContextsTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt index 8a4be016fa..64e6376701 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt @@ -21,6 +21,7 @@ package com.amaze.filemanager.filesystem.smb import android.content.Context +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -44,7 +45,7 @@ import org.robolectric.shadows.ShadowSQLiteConnection @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowSmbUtil::class, ShadowMultiDex::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) @LooperMode(LooperMode.Mode.PAUSED) class SmbHybridFileTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java index 1a30671ef4..cf2ddf36ec 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java @@ -46,6 +46,7 @@ import com.amaze.filemanager.filesystem.ssh.test.TestKeyProvider; import com.amaze.filemanager.shadows.ShadowMultiDex; +import android.os.Build; import android.os.Environment; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -57,7 +58,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public abstract class AbstractSftpServerTest { protected SshServer server; diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt index 687d29ebc6..4847947be9 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.filesystem.ssh +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -69,7 +70,7 @@ import java.security.KeyPair @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class NetCopyClientConnectionPoolSshTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt index cb85826740..8b317fda6e 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt @@ -21,6 +21,7 @@ package com.amaze.filemanager.filesystem.ssh import android.content.Context +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -45,7 +46,7 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class SshHybridFileTest { 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 3b9ce346ac..443dfe4d6c 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 @@ -41,6 +41,7 @@ import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockedConstruction; @@ -62,15 +63,18 @@ import com.amaze.filemanager.utils.PasswordUtil; import com.amaze.filemanager.utils.SmbUtil; +import android.Manifest; import android.os.Build; import android.os.Bundle; import android.os.storage.StorageManager; import android.util.Base64; +import androidx.annotation.RequiresApi; import androidx.lifecycle.Lifecycle; import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.GrantPermissionRule; import io.reactivex.android.plugins.RxAndroidPlugins; import io.reactivex.plugins.RxJavaPlugins; @@ -78,7 +82,7 @@ @RunWith(AndroidJUnit4.class) @Config( - sdk = {KITKAT, P}, + sdk = {KITKAT, P, Build.VERSION_CODES.R}, shadows = { ShadowMultiDex.class, ShadowStorageManager.class, @@ -97,6 +101,11 @@ public class MainActivityTest { "address", "port", "keypairName", "name", "username", "password", "edit" }; + @Rule + @RequiresApi(Build.VERSION_CODES.R) + public final GrantPermissionRule allFilesPermissionRule = + GrantPermissionRule.grant(Manifest.permission.MANAGE_EXTERNAL_STORAGE); + private MockedConstruction mc; @Before diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/PermissionsActivityTest.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/PermissionsActivityTest.kt new file mode 100644 index 0000000000..69741eb77e --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/PermissionsActivityTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.ui.activities + +import android.app.AppOpsManager +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Build.VERSION_CODES.KITKAT +import android.os.Build.VERSION_CODES.P +import android.os.Build.VERSION_CODES.R +import android.os.storage.StorageManager +import android.provider.Settings +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.afollestad.materialdialogs.DialogAction +import com.afollestad.materialdialogs.MaterialDialog +import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.TestUtils.initializeInternalStorage +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowDialog +import org.robolectric.shadows.ShadowSQLiteConnection +import org.robolectric.shadows.ShadowStorageManager + +/** + * Tests MainActivity's superclass, PermissionsActivity. + * + * Cannot instantiate itself, hence still uses MainActivity to trigger its actions. + */ +@RunWith(AndroidJUnit4::class) +@Config( + sdk = [KITKAT, P, Build.VERSION_CODES.R], + shadows = [ShadowMultiDex::class, ShadowStorageManager::class] +) +class PermissionsActivityTest { + + private lateinit var scenario: ActivityScenario + + /** + * Pre-test setup + */ + @Before + fun setUp() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) initializeInternalStorage() + RxJavaPlugins.reset() + RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.reset() + RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } + ShadowSQLiteConnection.reset() + } + + /** + * Post-test cleanup + */ + @After + fun tearDown() { + scenario.moveToState(Lifecycle.State.DESTROYED) + scenario.close() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Shadows.shadowOf( + ApplicationProvider.getApplicationContext().getSystemService( + StorageManager::class.java + ) + ).resetStorageVolumeList() + } + } + + /** + * Test grant all files access dialog. + */ + @Test + @Config(sdk = [R]) + fun testDisplayAllFilesPermissionDialog() { + scenario = ActivityScenario.launch(MainActivity::class.java) + scenario.moveToState(Lifecycle.State.STARTED) + scenario.onActivity { activity -> + // Make Environment.isExternalStorageManager() returns false. + val shadowApplication = shadowOf(RuntimeEnvironment.getApplication()) + shadowOf( + activity.getSystemService( + AppOpsManager::class.java + ) + ).setMode( + 92, + activity.applicationInfo.uid, + activity.packageName, + AppOpsManager.MODE_IGNORED + ) + activity.requestAllFilesAccess { } + assertNotNull(ShadowDialog.getLatestDialog()) + ShadowDialog.getLatestDialog().run { + assertTrue(this is MaterialDialog) + (this as MaterialDialog).run { + assertEquals( + activity.getString(com.amaze.filemanager.R.string.grantper), + this.titleView.text + ) + assertEquals( + activity.getString( + com.amaze.filemanager.R.string.grant_all_files_permission + ), + this.contentView?.text.toString() + ) + this.getActionButton(DialogAction.POSITIVE).run { + assertEquals( + activity.getString(com.amaze.filemanager.R.string.grant), + this.text + ) + performClick() + } + val intent = shadowApplication.nextStartedActivity + assertNotNull(intent) + assertEquals( + Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + intent.action + ) + assertEquals(Uri.parse("package:${activity.packageName}"), intent.data) + } + } + } + } +} diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java b/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java index 0ba4a68d69..e49fa6975b 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java @@ -48,6 +48,7 @@ import android.content.ContentResolver; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Environment; import android.widget.TextView; @@ -57,7 +58,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class TextEditorActivityTest { private final String fileContents = "fsdfsdfs"; diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt index 13cf133e23..ba3ac95807 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt @@ -20,19 +20,24 @@ package com.amaze.filemanager.ui.dialogs +import android.Manifest +import android.os.Build import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.N import android.os.Build.VERSION_CODES.P +import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowTabHandler import com.amaze.filemanager.test.TestUtils.initializeInternalStorage import com.amaze.filemanager.ui.activities.MainActivity import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -40,11 +45,20 @@ import org.robolectric.annotation.Config * Base class for various tests related to file encryption. */ @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class, ShadowTabHandler::class], sdk = [KITKAT, P]) +@Config( + shadows = [ShadowMultiDex::class, ShadowTabHandler::class], + sdk = [KITKAT, P, Build.VERSION_CODES.R] +) abstract class AbstractEncryptDialogTests { protected lateinit var scenario: ActivityScenario + @Rule + @JvmField + @RequiresApi(Build.VERSION_CODES.R) + var allFilesPermissionRule = GrantPermissionRule + .grant(Manifest.permission.MANAGE_EXTERNAL_STORAGE) + /** * MainActivity setup. */ diff --git a/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java b/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java index 7e2018a67c..2ae29be76a 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java @@ -33,12 +33,13 @@ import com.amaze.filemanager.database.CloudContract; import android.content.pm.PackageInfo; +import android.os.Build; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {KITKAT, P}) +@Config(sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class CloudSheetFragmentTest { @Test diff --git a/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java b/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java index f8873caf22..bc377b3a30 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java @@ -33,6 +33,7 @@ import com.amaze.filemanager.shadows.ShadowMultiDex; +import android.os.Build; import android.webkit.MimeTypeMap; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -40,7 +41,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class IconsTest { @Before diff --git a/app/src/test/java/com/amaze/filemanager/ui/notifications/NotificationConstantsTest.java b/app/src/test/java/com/amaze/filemanager/ui/notifications/NotificationConstantsTest.java index 84dac3f0b5..c2780642c3 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/notifications/NotificationConstantsTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/notifications/NotificationConstantsTest.java @@ -54,7 +54,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {KITKAT, P}) +@Config(sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class NotificationConstantsTest { private Context context; diff --git a/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt b/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt index 8e49c7e214..372d3360f9 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt @@ -22,6 +22,7 @@ package com.amaze.filemanager.ui.theme import android.content.Context import android.content.res.Configuration +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -36,7 +37,7 @@ import java.util.* @RunWith(AndroidJUnit4::class) @Config( - sdk = [KITKAT, P], + sdk = [KITKAT, P, Build.VERSION_CODES.R], shadows = [ShadowMultiDex::class] ) class AppThemeTest { diff --git a/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java b/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java index 032526308b..ecf1352a65 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java @@ -36,6 +36,7 @@ import com.amaze.filemanager.R; import android.content.Context; +import android.os.Build; import android.widget.Button; import android.widget.EditText; @@ -45,7 +46,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {KITKAT, P}) +@Config(sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class WarnableTextInputValidatorTest { private Context context; diff --git a/app/src/test/java/com/amaze/filemanager/utils/AESCryptTest.kt b/app/src/test/java/com/amaze/filemanager/utils/AESCryptTest.kt index 662598e6ef..a1c7cbbcad 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/AESCryptTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/AESCryptTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.utils +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -35,7 +36,7 @@ import kotlin.random.Random * Unit test for [AESCrypt] */ @RunWith(AndroidJUnit4::class) -@Config(sdk = [KITKAT, P]) +@Config(sdk = [KITKAT, P, Build.VERSION_CODES.R]) class AESCryptTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/utils/AnimUtilsTest.java b/app/src/test/java/com/amaze/filemanager/utils/AnimUtilsTest.java index 4434414b14..b5cbfc2003 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/AnimUtilsTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/AnimUtilsTest.java @@ -38,13 +38,14 @@ import com.amaze.filemanager.ui.views.ThemedTextView; +import android.os.Build; import android.view.animation.Interpolator; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {KITKAT, P}) +@Config(sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class AnimUtilsTest { @Test diff --git a/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt index e0494d24de..2cd21af630 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.utils +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -47,7 +48,7 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) @Config( - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class CryptUtilTest { diff --git a/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt b/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt index 10017c5985..9ecadd5fab 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.utils +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.text.SpannedString @@ -31,7 +32,7 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) -@Config(sdk = [KITKAT, P]) +@Config(sdk = [KITKAT, P, Build.VERSION_CODES.R]) class MinMaxInputFilterTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt index df499decb9..635547e8f0 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.utils +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider @@ -46,7 +47,7 @@ import org.robolectric.annotation.Config @Suppress("StringLiteralDuplication") @RunWith(AndroidJUnit4::class) @Config( - sdk = [KITKAT, P], + sdk = [KITKAT, P, Build.VERSION_CODES.R], shadows = [ShadowPasswordUtil::class, ShadowSmbUtil::class] ) class SmbUtilTest { diff --git a/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java b/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java index db778827d3..67b03d58b2 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java @@ -31,13 +31,14 @@ import org.robolectric.annotation.Config; import android.content.SharedPreferences; +import android.os.Build; import androidx.preference.PreferenceManager; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {KITKAT, P}) +@Config(sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class TinyDBTest { private SharedPreferences prefs; diff --git a/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java b/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java index a53083910f..0c8d6dd710 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java @@ -56,7 +56,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {KITKAT, P}) +@Config(sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class UtilsTest { @Test diff --git a/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt index 3b4aee022e..9aebb4de92 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.utils +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -42,7 +43,7 @@ import javax.security.cert.X509Certificate @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [KITKAT, P] + sdk = [KITKAT, P, Build.VERSION_CODES.R] ) class X509CertificateUtilTest { diff --git a/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java b/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java index 12e9be1360..0f161a8256 100644 --- a/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java +++ b/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java @@ -43,6 +43,7 @@ import com.amaze.filemanager.shadows.ShadowMultiDex; import com.amaze.filemanager.utils.PasswordUtil; +import android.os.Build; import android.util.Base64; import androidx.test.core.app.ApplicationProvider; @@ -55,7 +56,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class, ShadowPasswordUtil.class}, - sdk = {KITKAT, P}) + sdk = {KITKAT, P, Build.VERSION_CODES.R}) public class ShadowPasswordUtilTest { @Before diff --git a/testShared/src/test/java/com/amaze/filemanager/test/TestUtils.kt b/testShared/src/test/java/com/amaze/filemanager/test/TestUtils.kt index c094b62a9b..e0ce8a764b 100644 --- a/testShared/src/test/java/com/amaze/filemanager/test/TestUtils.kt +++ b/testShared/src/test/java/com/amaze/filemanager/test/TestUtils.kt @@ -22,6 +22,7 @@ package com.amaze.filemanager.test import android.content.Context import android.os.Build +import android.os.Build.VERSION_CODES import android.os.Environment import android.os.Parcel import android.os.UserHandle @@ -33,6 +34,7 @@ import com.amaze.filemanager.BuildConfig import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.compressed.CompressedHelper import org.robolectric.Shadows +import org.robolectric.shadows.ShadowEnvironment import java.lang.reflect.Field import java.lang.reflect.Modifier import kotlin.random.Random @@ -99,6 +101,15 @@ object TestUtils { parcel.writeString("1234-5678") parcel.writeString(Environment.MEDIA_MOUNTED) addVolumeToStorageManager(parcel) + + /* + * Monkey-patch ShadowEnvironment for Environment.isExternalStorageManager() to work. + * + * See https://github.com/robolectric/robolectric/issues/7300 + */ + if (Build.VERSION.SDK_INT >= VERSION_CODES.R) { + ShadowEnvironment.addExternalDir(Environment.getExternalStorageDirectory().absolutePath) + } } /** From 15c32ef28b406f2b5a73a8a5a652473ebbcb621e Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sun, 25 Dec 2022 01:57:17 +0800 Subject: [PATCH 041/384] Convert PemToKeyPairTask to PemToKeyPairObservable Fixes #1364. At code level it's reusing the same class, but with RxJava's Observable should do things better than AsyncTasks anyway. --- .github/workflows/android-feature.yml | 7 - ...yPairTask.kt => PemToKeyPairObservable.kt} | 149 ++++----- .../ftp/NetCopyClientConnectionPool.kt | 23 +- .../ui/dialogs/SftpConnectDialog.kt | 46 ++- .../main/res/layout/dialog_singleedittext.xml | 2 +- app/src/main/res/values/strings.xml | 1 + .../ssh/PemToKeyPairObservableEd25519Test.kt | 124 ++++++++ .../ssh/PemToKeyPairObservableRsaTest.kt | 301 ++++++++++++++++++ .../asynctasks/ssh/PemToKeyPairTaskTest.java | 131 -------- .../asynctasks/ssh/PemToKeyPairTaskTest2.java | 98 ------ 10 files changed, 536 insertions(+), 346 deletions(-) rename app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/{PemToKeyPairTask.kt => PemToKeyPairObservable.kt} (60%) create mode 100644 app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt create mode 100644 app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt delete mode 100644 app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest.java delete mode 100644 app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest2.java diff --git a/.github/workflows/android-feature.yml b/.github/workflows/android-feature.yml index 271679d775..dc1cc3f8f5 100644 --- a/.github/workflows/android-feature.yml +++ b/.github/workflows/android-feature.yml @@ -45,10 +45,3 @@ jobs: uses: gradle/gradle-build-action@v2 with: arguments: jacocoTestPlayDebugUnitTestReport --stacktrace --info - - name: Publish test results - uses: dorny/test-reporter@v1 - if: always() - with: - name: test-results - path: 'app/build/test-results/testPlayDebugUnitTest/TEST-*.xml' - reporter: java-junit \ No newline at end of file diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservable.kt similarity index 60% rename from app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTask.kt rename to app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservable.kt index aa15cc986f..501be08903 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTask.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservable.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Copyright (C) 2014-2022 Arpit Khurana , Vishal Nehra , * Emmanuel Messulam, Raymond Lai and Contributors. * * This file is part of Amaze File Manager. @@ -20,22 +20,19 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh -import android.os.AsyncTask import android.text.InputType -import android.view.View -import android.widget.EditText +import android.view.LayoutInflater import android.widget.Toast import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog import com.amaze.filemanager.R import com.amaze.filemanager.application.AppConfig -import com.amaze.filemanager.asynchronous.asynctasks.AsyncTaskResult +import com.amaze.filemanager.databinding.DialogSingleedittextBinding import com.amaze.filemanager.ui.views.WarnableTextInputLayout import com.amaze.filemanager.ui.views.WarnableTextInputValidator -import com.amaze.filemanager.ui.views.WarnableTextInputValidator.ReturnState import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile -import net.schmizz.sshj.common.IOUtils -import net.schmizz.sshj.userauth.keyprovider.KeyProvider +import io.reactivex.ObservableEmitter +import io.reactivex.ObservableOnSubscribe import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile import net.schmizz.sshj.userauth.password.PasswordFinder @@ -50,86 +47,67 @@ import java.io.InputStream import java.io.StringReader import java.security.KeyPair -/** - * [AsyncTask] to convert given [InputStream] into [KeyPair] which is requird by - * sshj, using [JcaPEMKeyConverter]. - * - * @see JcaPEMKeyConverter - * - * @see KeyProvider - * - * @see OpenSSHKeyV1KeyFile - * - * @see PuTTYKeyFile - * - * @see com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.create - * @see net.schmizz.sshj.SSHClient.authPublickey - */ -class PemToKeyPairTask( - private val pemFile: ByteArray, - private val callback: AsyncTaskResult.Callback -) : - AsyncTask() { +class PemToKeyPairObservable(private val pemFile: ByteArray) : ObservableOnSubscribe { + private val converters = arrayOf( JcaPemToKeyPairConverter(), OpenSshPemToKeyPairConverter(), OpenSshV1PemToKeyPairConverter(), PuttyPrivateKeyToKeyPairConverter() ) - private val log: Logger = LoggerFactory.getLogger(PemToKeyPairTask::class.java) - - private var paused = false private var passwordFinder: PasswordFinder? = null private var errorMessage: String? = null - constructor(pemFile: InputStream, callback: AsyncTaskResult.Callback) : - this(IOUtils.readFully(pemFile).toByteArray(), callback) - constructor(pemContent: String, callback: AsyncTaskResult.Callback) : - this(pemContent.toByteArray(), callback) + companion object { + private val log: Logger = LoggerFactory.getLogger(PemToKeyPairObservable::class.java) + } - override fun doInBackground(vararg voids: Void): KeyPair? { - while (true) { - if (isCancelled) { - return null - } - if (paused) { - continue - } - for (converter in converters) { - val keyPair = converter.convert(String(pemFile)) - if (keyPair != null) { - paused = false - return keyPair - } - } - if (passwordFinder != null) { - errorMessage = AppConfig - .getInstance() - .getString(R.string.ssh_key_invalid_passphrase) + constructor(pemFile: InputStream) : this(pemFile.readBytes()) + constructor(pemContent: String) : this(pemContent.toByteArray()) + + override fun subscribe(emitter: ObservableEmitter) { + for (converter in converters) { + val keyPair = converter.convert(String(pemFile)) + if (keyPair != null) { + emitter.onNext(keyPair) + emitter.onComplete() + return } - paused = true - publishProgress(IOException("No converter available to parse selected PEM")) } + if (passwordFinder != null) { + errorMessage = AppConfig + .getInstance() + .getString(R.string.ssh_key_invalid_passphrase) + } else { + errorMessage = AppConfig + .getInstance() + .getString(R.string.ssh_key_no_decoder_decrypt) + } + emitter.onError(IOException(errorMessage)) } - override fun onProgressUpdate(vararg values: IOException?) { - super.onProgressUpdate(*values) - if (values.isEmpty()) { - return - } - val result = values[0] - val builder = MaterialDialog.Builder(AppConfig.getInstance().mainActivityContext!!) - val dialogLayout = View.inflate( - AppConfig.getInstance().mainActivityContext, - R.layout.dialog_singleedittext, - null + /** + * For generating the callback when decoding the PEM failed. Opens dialog and prompt for + * password. + */ + fun displayPassphraseDialog( + exception: Throwable, + positiveCallback: (() -> Unit), + negativeCallback: (() -> Unit) + ) { + val builder = MaterialDialog.Builder( + AppConfig.getInstance().mainActivityContext!! + ) + val dialogLayout = DialogSingleedittextBinding.inflate( + LayoutInflater.from(AppConfig.getInstance().mainActivityContext) ) val wilTextfield: WarnableTextInputLayout = - dialogLayout.findViewById(R.id.singleedittext_warnabletextinputlayout) - val textfield = dialogLayout.findViewById(R.id.singleedittext_input) - textfield.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + dialogLayout.singleedittextWarnabletextinputlayout + val textfield = dialogLayout.singleedittextInput + textfield.inputType = InputType.TYPE_CLASS_TEXT or + InputType.TYPE_TEXT_VARIATION_PASSWORD builder - .customView(dialogLayout, false) + .customView(dialogLayout.root, false) .autoDismiss(false) .title(R.string.ssh_key_prompt_passphrase) .positiveText(R.string.ok) @@ -138,19 +116,18 @@ class PemToKeyPairTask( override fun reqPassword(resource: Resource<*>?): CharArray { return textfield.text.toString().toCharArray() } - override fun shouldRetry(resource: Resource<*>?): Boolean { return false } } - paused = false dialog.dismiss() + positiveCallback.invoke() } .negativeText(R.string.cancel) .onNegative { dialog: MaterialDialog, which: DialogAction? -> dialog.dismiss() - toastOnParseError(result!!) - cancel(true) + toastOnParseError(exception) + negativeCallback.invoke() } val dialog = builder.show() WarnableTextInputValidator( @@ -160,12 +137,12 @@ class PemToKeyPairTask( dialog.getActionButton(DialogAction.POSITIVE) ) { text: String -> if (text.isEmpty()) { - ReturnState( - ReturnState.STATE_ERROR, + WarnableTextInputValidator.ReturnState( + WarnableTextInputValidator.ReturnState.STATE_ERROR, R.string.field_empty ) } - ReturnState() + WarnableTextInputValidator.ReturnState() } if (errorMessage != null) { wilTextfield.error = errorMessage @@ -173,11 +150,7 @@ class PemToKeyPairTask( } } - override fun onPostExecute(result: KeyPair?) { - callback.onResult(result) - } - - private fun toastOnParseError(result: IOException) { + private fun toastOnParseError(result: Throwable) { Toast.makeText( AppConfig.getInstance().mainActivityContext, AppConfig.getInstance() @@ -189,18 +162,16 @@ class PemToKeyPairTask( } private abstract inner class PemToKeyPairConverter { - fun convert(source: String?): KeyPair? = runCatching { + fun convert(source: String): KeyPair? = runCatching { throwingConvert(source) }.onFailure { log.warn("failed to convert pem to keypair", it) }.getOrNull() - @Throws(Exception::class) protected abstract fun throwingConvert(source: String?): KeyPair? } private inner class JcaPemToKeyPairConverter : PemToKeyPairConverter() { - @Throws(Exception::class) override fun throwingConvert(source: String?): KeyPair? { val pemParser = PEMParser(StringReader(source)) val keyPair = pemParser.readObject() as PEMKeyPair? @@ -210,8 +181,7 @@ class PemToKeyPairTask( } private inner class OpenSshPemToKeyPairConverter : PemToKeyPairConverter() { - @Throws(Exception::class) - public override fun throwingConvert(source: String?): KeyPair { + override fun throwingConvert(source: String?): KeyPair { val converter = OpenSSHKeyFile() converter.init(StringReader(source), passwordFinder) return KeyPair(converter.public, converter.private) @@ -219,8 +189,7 @@ class PemToKeyPairTask( } private inner class OpenSshV1PemToKeyPairConverter : PemToKeyPairConverter() { - @Throws(Exception::class) - public override fun throwingConvert(source: String?): KeyPair { + override fun throwingConvert(source: String?): KeyPair { val converter = OpenSSHKeyV1KeyFile() converter.init(StringReader(source), passwordFinder) return KeyPair(converter.public, converter.private) diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt index 56374a6693..f3fd9b928d 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt @@ -23,13 +23,15 @@ package com.amaze.filemanager.filesystem.ftp import android.annotation.SuppressLint import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.asynchronous.asynctasks.ftp.auth.FtpAuthenticationTask -import com.amaze.filemanager.asynchronous.asynctasks.ssh.PemToKeyPairTask +import com.amaze.filemanager.asynchronous.asynctasks.ssh.PemToKeyPairObservable import com.amaze.filemanager.asynchronous.asynctasks.ssh.SshAuthenticationTask import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils.extractBaseUriFrom import com.amaze.filemanager.utils.PasswordUtil import io.reactivex.Flowable import io.reactivex.Maybe +import io.reactivex.Observable.create import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import net.schmizz.sshj.Config import net.schmizz.sshj.SSHClient @@ -254,10 +256,23 @@ object NetCopyClientConnectionPool { val pem = utilsHandler.getSshAuthPrivateKey(url) val keyPair = AtomicReference(null) if (true == pem?.isNotEmpty()) { + val observable = PemToKeyPairObservable(pem) keyPair.set( - PemToKeyPairTask( - pem - ) { }.execute().get() + create(observable) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retryWhen { exceptions -> + exceptions.flatMap { exception -> + create { subscriber -> + observable.displayPassphraseDialog(exception, { + subscriber.onNext(Unit) + }, { + subscriber.onError(exception) + }) + } + } + } + .blockingFirst() ) } val hostKey = utilsHandler.getRemoteHostKey(url) ?: return null 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 1d2e125298..bbef37767d 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 @@ -46,7 +46,7 @@ import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.asynchronous.asynctasks.ftp.AbstractGetHostInfoTask import com.amaze.filemanager.asynchronous.asynctasks.ftp.hostcert.FtpsGetHostCertificateTask import com.amaze.filemanager.asynchronous.asynctasks.ssh.GetSshHostFingerprintTask -import com.amaze.filemanager.asynchronous.asynctasks.ssh.PemToKeyPairTask +import com.amaze.filemanager.asynchronous.asynctasks.ssh.PemToKeyPairObservable import com.amaze.filemanager.database.UtilsHandler import com.amaze.filemanager.database.models.OperationData import com.amaze.filemanager.databinding.SftpDialogBinding @@ -66,6 +66,8 @@ import com.amaze.filemanager.utils.MinMaxInputFilter import com.amaze.filemanager.utils.SimpleTextWatcher import com.amaze.filemanager.utils.X509CertificateUtil.FINGERPRINT import com.google.android.material.snackbar.Snackbar +import io.reactivex.Observable +import io.reactivex.Observable.create import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers @@ -579,19 +581,33 @@ class SftpConnectDialog : DialogFragment() { runCatching { requireContext().contentResolver.openInputStream(this)?.let { selectedKeyContent -> - PemToKeyPairTask(selectedKeyContent) { result: KeyPair? -> - selectedParsedKeyPair = result - selectedParsedKeyPairName = this - .lastPathSegment!! - .substring( - this.lastPathSegment!! - .indexOf('/') + 1 - ) - val okBTN = (dialog as MaterialDialog) - .getActionButton(DialogAction.POSITIVE) - okBTN.isEnabled = okBTN.isEnabled || true - binding.selectPemBTN.text = selectedParsedKeyPairName - }.execute() + val observable = PemToKeyPairObservable(selectedKeyContent) + create(observable).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retryWhen { exceptions -> + exceptions.flatMap { exception -> + Observable.create { subscriber -> + observable.displayPassphraseDialog(exception, { + subscriber.onNext(Unit) + }, { + subscriber.onError(exception) + }) + } + } + } + .subscribe({ result -> + selectedParsedKeyPair = result + selectedParsedKeyPairName = this + .lastPathSegment!! + .substring( + this.lastPathSegment!! + .indexOf('/') + 1 + ) + val okBTN = (dialog as MaterialDialog) + .getActionButton(DialogAction.POSITIVE) + okBTN.isEnabled = okBTN.isEnabled || true + binding.selectPemBTN.text = selectedParsedKeyPairName + }, {}) } }.onFailure { log.error("Error reading PEM key", it) @@ -694,7 +710,7 @@ class SftpConnectDialog : DialogFragment() { ): Boolean { DataUtils.getInstance().removeServer(DataUtils.getInstance().containsServer(oldPath)) DataUtils.getInstance().addServer(arrayOf(connectionName, encryptedPath)) - Collections.sort(DataUtils.getInstance().servers, BookSorter()) + DataUtils.getInstance().servers.sortWith(BookSorter()) (activity as MainActivity).drawer.refreshDrawer() AppConfig.getInstance().runInBackground { AppConfig.getInstance().utilsHandler.updateSsh( diff --git a/app/src/main/res/layout/dialog_singleedittext.xml b/app/src/main/res/layout/dialog_singleedittext.xml index 9f7b3eca8c..63489887e5 100644 --- a/app/src/main/res/layout/dialog_singleedittext.xml +++ b/app/src/main/res/layout/dialog_singleedittext.xml @@ -11,7 +11,7 @@ android:paddingLeft="@dimen/md_dialog_frame_margin" android:paddingRight="@dimen/md_dialog_frame_margin"> - It\'s recommended to use \".txt\" as the file extension. Select private key for authentication Invalid key passphrase. + No valid decoder to decrypt PEM. Failed to update encryption entry in database Extracted Compressed diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt new file mode 100644 index 0000000000..f58cfdcc01 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt @@ -0,0 +1,124 @@ +/* + * 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.asynchronous.asynctasks.ssh + +import android.os.Build.VERSION_CODES.JELLY_BEAN +import android.os.Build.VERSION_CODES.KITKAT +import android.os.Build.VERSION_CODES.P +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.ShadowTabHandler +import io.reactivex.Observable +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import net.schmizz.sshj.userauth.password.PasswordFinder +import net.schmizz.sshj.userauth.password.Resource +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +/** + * Test [PemToKeyPairObservable] for ed25519 keys. + */ +@RunWith(AndroidJUnit4::class) +@Config( + shadows = [ShadowMultiDex::class, ShadowTabHandler::class], + sdk = [JELLY_BEAN, KITKAT, P] +) +class PemToKeyPairObservableEd25519Test { + + companion object { + // public key for authorized_keys: ssh-ed25519 + // AAAAC3NzaC1lZDI1NTE5AAAAIGxJHFewxU9tJn9hUq9e2C/+ELFw83NpmJ5NLFOzU7O3 test-openssh-key + private const val unencryptedOpenSshKey = ( + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n" + + "QyNTUxOQAAACBsSRxXsMVPbSZ/YVKvXtgv/hCxcPNzaZieTSxTs1OztwAAAJhX2WUxV9ll\n" + + "MQAAAAtzc2gtZWQyNTUxOQAAACBsSRxXsMVPbSZ/YVKvXtgv/hCxcPNzaZieTSxTs1Oztw\n" + + "AAAECjSjwwMXPzbZWq/EBoA4HA9Lr7B1/Tw78K+k1zqAJwA2xJHFewxU9tJn9hUq9e2C/+\n" + + "ELFw83NpmJ5NLFOzU7O3AAAADmFpcndhdmVAaHN2MDEwAQIDBAUGBw==\n" + + "-----END OPENSSH PRIVATE KEY-----" + ) + + // Passphrase = 12345678 + // public key for authorized_keys: ssh-ed25519 + // AAAAC3NzaC1lZDI1NTE5AAAAIHio1/33U0XoewL1qGLmTzxyVNeYP5b0Tunv/SQrQi92 test-openssh-key + private const val encryptedOpenSshKey = ( + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCwlfECA9\n" + + "+EGLwKVApTmomnAAAAZAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHio1/33U0XoewL1\n" + + "qGLmTzxyVNeYP5b0Tunv/SQrQi92AAAAoD2dysYInLaJgXIv6k/xFv7OblU9vWkCwcYnDW\n" + + "8Zj5+ke8QL2/r7EUBEvY1H02GenlEH1Ufct8ce7/eAWwd7aWukaSQXlKW9IBt5YrxW8+P/\n" + + "wrHcd/Z92eQ0E7NV6b6LnghGYlyCjpSBW+mxa0AAYPD21c95d/HvJF6zxQl/IKCCLdOrr/\n" + + "ilMCSIGQEdg71hA3MMZsRbUvazsnZTZXD9PLI=\n" + + "-----END OPENSSH PRIVATE KEY-----" + ) + } + + /** + * Pre test setup. + */ + @Before + fun setUp() { + RxJavaPlugins.reset() + RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.reset() + RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } + } + + /** + * Test decrypt unencrypted key pair. + */ + @Test + fun testUnencryptedKeyToKeyPair() { + val task = PemToKeyPairObservable(unencryptedOpenSshKey) + val result = Observable.create(task).subscribeOn(Schedulers.single()).blockingFirst() + Assert.assertNotNull(result) + Assert.assertNotNull(result.public) + Assert.assertNotNull(result.private) + } + + /** + * Test decrypt passphrase protected key pair. + */ + @Test + fun testEncryptedKeyToKeyPair() { + val task = PemToKeyPairObservable(encryptedOpenSshKey) + val field = PemToKeyPairObservable::class.java.getDeclaredField("passwordFinder") + field.isAccessible = true + field[task] = object : PasswordFinder { + override fun reqPassword(resource: Resource<*>?): CharArray { + return "12345678".toCharArray() + } + + override fun shouldRetry(resource: Resource<*>?): Boolean { + return false + } + } + val result = Observable.create(task).subscribeOn(Schedulers.single()).blockingFirst() + Assert.assertNotNull(result) + Assert.assertNotNull(result.public) + Assert.assertNotNull(result.private) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt new file mode 100644 index 0000000000..1192f5e6ad --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt @@ -0,0 +1,301 @@ +/* + * 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.asynchronous.asynctasks.ssh + +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES.JELLY_BEAN +import android.os.Build.VERSION_CODES.KITKAT +import android.os.Build.VERSION_CODES.N +import android.os.Build.VERSION_CODES.P +import android.widget.EditText +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.afollestad.materialdialogs.DialogAction +import com.afollestad.materialdialogs.MaterialDialog +import com.amaze.filemanager.R +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.ShadowTabHandler +import com.amaze.filemanager.test.TestUtils +import com.amaze.filemanager.ui.activities.MainActivity +import io.reactivex.Observable +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import net.schmizz.sshj.userauth.password.PasswordFinder +import net.schmizz.sshj.userauth.password.Resource +import org.awaitility.Awaitility.await +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowDialog +import org.robolectric.shadows.ShadowToast +import java.security.KeyPair +import java.util.concurrent.TimeUnit + +/** + * Test [PemToKeyPairObservable]. + */ +@RunWith(AndroidJUnit4::class) +@Config( + shadows = [ShadowMultiDex::class, ShadowTabHandler::class], + sdk = [JELLY_BEAN, KITKAT, P] +) +class PemToKeyPairObservableRsaTest { + + companion object { + private const val unencryptedPuttyKey = ( + "PuTTY-User-Key-File-2: ssh-rsa\n" + + "Encryption: none\n" + + "Comment: Key for test only\n" + + "Public-Lines: 6\n" + + "AAAAB3NzaC1yc2EAAAABJQAAAQEA6ZhWkS0Xpb1riC5r3dulviIwVFUP4uXnnapv\n" + + "eqVwB/7aklhu3SnOlBwRMoan+AohhHogy2cjNMqW6x/xLwH9Cbo4kMTeJTPR7ca2\n" + + "lxmCtGgdFRheR84if6T+i2fb1ADUmncJEkL2H1Q7RG7+opoerDpwdGjopsP7s7H4\n" + + "ZTvGGXcudhrOFOf/gW8hR8m9wJ05ON8qfKiRWIKDxFpectFOpJC/NGP4F53EHNAk\n" + + "HIhNPSW5voytGvj4VaS/xRAs2HLmj7jTor/Le/vJlndmnyJkGIwEJbVpp5HsZG7R\n" + + "VyhqZwCcI6ZiMXvYSH6oplffUGZz5HXkskBmreMauZC1beN31Q==\n" + + "Private-Lines: 14\n" + + "AAABAD8iQOj3bizLaSu5hO/af9KFx99w74lukaA75sczobz4xXOpMrhQfQVvXpgI\n" + + "t8Z/R1Q8rurdms//Zw8dY8eD/zMPvELNbHjB5bXireOmB6ZhU/fdEo/yhd1PMAoA\n" + + "ZO0wqCm/To9QXjH7Fu/mpa9n7J1AOhGf0C0SX7QGlikyv+s0c4ib+ipR6TEoLRHD\n" + + "Oa85soSZGjPvPckkfSNncemEWuo4Hp0jiJOU/Gd37YNY4Jc9FhRRuBlTUvhBdg5g\n" + + "FsvHZD2fZ+5J0Z2Gm9tJL6Uq6rvNWVC9sFlnornPXM9/UvXQ1Q59rIk0CQNEOVr7\n" + + "kdYpYeUhYrrwhCVQrjbV8CxyRi0AAACBAP1T4/nXa/+tA9f3orMBkxzQF0FnPI/+\n" + + "e8YXvdHtkHl3/uJEEy9FmliJecKNtNBDN6Tu3iAs5ne/btvyduMgRAxqOyXxdQCq\n" + + "uR2iNoHLCDqgOUbXCh+swVHPXsdbbhv/54aWjLBbbfZ6S1CwTmPV3eAVJRb78JwS\n" + + "uBi8Sq/5ZnhFAAAAgQDsDyjp1Bm6nonVwaGCHRsH5JFuiHbP8cyIQzdnC6PrMFvS\n" + + "EU1PfMKSeudywnEKyljct7Njw5FnMt1InWj1X80adK/gWTmppAPRr0u0ipT4J9ry\n" + + "1yCj4zeNS/cylZZoP/rOCq8Z2mbzqIO28jN5e7xDMrutNdXhqrOwmMgM0AliUQAA\n" + + "AIEAjMoM3mw2YXE7U1X2H/hfYymMWC+6XU4XHCI2Fk+CWGUPxvDT3uqUtoEXOXkY\n" + + "THdPSgA2f6EmqCOPR1VAA4jdQkK8VkN3/O3zWFdfRGqN5Kka7a7cmcyd93sq3LIU\n" + + "EYe4EYW7BQwe1W5ZCO+lRzjquGAB5rMhdAnzYfvkPc7sfJ8=\n" + + "Private-MAC: 2cd5ec740c5dd854e8a6bea3773f98697670bdc6" + ) + + // Passphrase = test + private const val encryptedPuttyKey = ( + "PuTTY-User-Key-File-2: ssh-rsa\n" + + "Encryption: aes256-cbc\n" + + "Comment: Key for test only\n" + + "Public-Lines: 6\n" + + "AAAAB3NzaC1yc2EAAAABJQAAAQEAoArpfCYeHImHcTELKVzVjyS6N6viAN4lzkWC\n" + + "EDCyBX6x4wwgVXRYQTbd6xNCpVb/TdBTN/aVF9EXtMW2TXyvntxGblE+ilK8b3GL\n" + + "zRfxjrjGsjqffwlHn3JaWpCOYtEqgGOLeKkofbKBXGn1aK6tvowPsY89Dl4aK857\n" + + "mwisAvCIxmd8b6f2aBy4MLQ7AdmZXxPq6YD9CDXPyQkNG4RH5RGAIAw7jD+O6tUo\n" + + "g69voQudjy3D51VQDGNxOJVojQiQvmRUR2qkSazPJqE/hFsdN6rKh+Pbe8h6z+rU\n" + + "bym37+sTK5JwWKHDQ7/ezLdNR38wAPHdz2VW5+0rqKm8LAtCGQ==\n" + + "Private-Lines: 14\n" + + "erS8mfUDEme+ujzF1k0GA7d2P4umHriMFQjBZIdvht6amZXoF1L+bkJp82/vG9lv\n" + + "YYNYQqk5tHezkV3sJncPwr3RI/0Y1L+WtKWSfE6OzSKdYJoX3WpAuMTeMlVrxu0t\n" + + "RXnjbfSz7Z1czryxO4NgAK3NsYQK6h290uq5/mpqP6fIhT3/tn+mH8kihAt8+uum\n" + + "1RW9ucNi3TZTG4I00Z3LWHw1VaqYFeCYh3yp8Canv3mKGn5ISqsd5ehNXW1TYvJF\n" + + "Bd742+JlxK8dhrAr2R+g+erSA0ac8Df3wH72syVPzdewnh+21zff3NGI9GWN799X\n" + + "CnVtf+psDPuebGQIHewNTGsaziNkAT5rGXdNMo3Xln/B1Wr9l8tIJAtDWSNqjDLp\n" + + "kAcLQ+Z1wTPZehZKBi0oTsLVm4tEcPQsbnuK9h+Y/d9EWcmBEiHTGM6otesNZNA8\n" + + "i247YZpyrG3azeRFBVMNSzKJ+vS2rKpgvm8nbYKy+nO5uNZDEm9oARr9QPCxdzXK\n" + + "dmI9F8IT3tLd4qCekD4DI9MKxJLjzFmyGOHc4zMxgUyL5BT5suVDIAWL/hiekwjt\n" + + "T1+V+TRScq4c+pIWxfVu4kY+HpLUpSR3RAVaRFar7jaB+YEEXw+gqEVTCyZeXGFf\n" + + "dWU+8BkhFBF24v3Qoi9SmuWYrSQGl9O8smIHW0H2JCNF+8oqpQG8dwx37L3VyMNq\n" + + "ApJh0LnRhoRwKo2YaAZKInaFTYS8Gnj7DvZ+l7lxPRfCV5yl9U2How2BI9YPRDDu\n" + + "Gs4agAG3InJnMiuIOzaNOIFLGM9STtYNyvG411rj6tR4EEQ6cJCxIlVe5a1mEt7M\n" + + "GVfbB5wUvow0o0a56OBmFMZOCxV2Vpxu6PuGTD8QQ0O0YzNDWFk3Fj2RRnnLCBLF\n" + + "Private-MAC: f742e2954fbb0c98984db0d9855a0f15507ecc0a" + ) + } + + private lateinit var scenario: ActivityScenario + + /** + * Pre test setup. + */ + @Before + fun setUp() { + RxJavaPlugins.reset() + RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.reset() + RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } + if (SDK_INT >= N) TestUtils.initializeInternalStorage() + scenario = ActivityScenario.launch(MainActivity::class.java) + scenario.moveToState(Lifecycle.State.STARTED) + } + + /** + * Post test clean up. + */ + @After + fun tearDown() { + scenario.close() + } + + /** + * Test decrypt unencrypted key pair. + */ + @Test + fun testUnencryptedKeyToKeyPair() { + val task = PemToKeyPairObservable(unencryptedPuttyKey) + val result = Observable.create(task).subscribeOn(Schedulers.single()).blockingFirst() + assertNotNull(result) + assertNotNull(result?.public) + assertNotNull(result?.private) + } + + /** + * Test decrypt passphrase protected key pair. + */ + @Test + fun testEncryptedKeyToKeyPair() { + val task = PemToKeyPairObservable(encryptedPuttyKey) + val field = PemToKeyPairObservable::class.java.getDeclaredField("passwordFinder") + field.isAccessible = true + field[task] = object : PasswordFinder { + override fun reqPassword(resource: Resource<*>): CharArray = "test".toCharArray() + override fun shouldRetry(resource: Resource<*>): Boolean = false + } + val result = Observable.create(task).subscribeOn(Schedulers.single()).blockingFirst() + assertNotNull(result) + assertNotNull(result?.public) + assertNotNull(result?.private) + } + + /** + * Test decrypt passphrase protected key pair with wrong passphrase, then a correct passphrase. + */ + @Test + fun testEncryptedKeyToKeyPairWithWrongPassphrase() { + performTestInActivity { + var lap = 0 + val task = PemToKeyPairObservable(encryptedPuttyKey) + val field = PemToKeyPairObservable::class.java.getDeclaredField("passwordFinder") + var result: KeyPair? = null + field.isAccessible = true + field[task] = object : PasswordFinder { + override fun reqPassword(resource: Resource<*>): CharArray = "foobar".toCharArray() + override fun shouldRetry(resource: Resource<*>): Boolean = false + } + Observable.create(task).subscribeOn(Schedulers.io()) + .retryWhen { exceptions -> + exceptions.flatMap { exception -> + Observable.create { subscriber -> + task.displayPassphraseDialog(exception, { + subscriber.onNext(Unit) + }, { + subscriber.onError(exception) + }) + } + } + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + if (lap == 0) { + fail("Should not return KeyPair") + } else { + result = it + } + }, { + if (lap > 0) { + fail("Cannot decrypt keypair") + } + }) + await().atMost(10, TimeUnit.SECONDS).until { + ShadowDialog.getLatestDialog() != null + } + (ShadowDialog.getLatestDialog() as MaterialDialog).let { dialog -> + assertEquals( + AppConfig.getInstance().resources.getText(R.string.ssh_key_prompt_passphrase), + dialog.titleView.text + ) + dialog.customView?.run { + lap++ + findViewById(R.id.singleedittext_input)?.run { + this.setText("test") + } ?: fail("Text field not found") + } ?: fail("No view found at dialog") + dialog.getActionButton(DialogAction.POSITIVE).performClick() + } + await().atMost(30, TimeUnit.SECONDS).until { result != null } + assertNotNull(result?.public) + assertNotNull(result?.private) + } + } + + /** + * Test decrypt passphrase protected key pair with wrong passphrase, then cancel. + */ + @Test + fun testEncryptedKeyToKeyPairWithWrongPassphraseThenCancel() { + performTestInActivity { + val task = PemToKeyPairObservable(encryptedPuttyKey) + Observable.create(task).subscribeOn(Schedulers.io()) + .retryWhen { exceptions -> + exceptions.flatMap { exception -> + Observable.create { subscriber -> + task.displayPassphraseDialog(exception, { + subscriber.onNext(Unit) + }, { + subscriber.onError(exception) + }) + } + } + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + fail("Should not return KeyPair") + }, {}) + await().atMost(10, TimeUnit.SECONDS).until { + ShadowDialog.getLatestDialog() != null + } + (ShadowDialog.getLatestDialog() as MaterialDialog).let { dialog -> + assertEquals( + AppConfig.getInstance().resources.getText(R.string.ssh_key_prompt_passphrase), + dialog.titleView.text + ) + dialog.getActionButton(DialogAction.NEGATIVE).performClick() + } + await().atMost(30, TimeUnit.SECONDS).until { + ShadowToast.getLatestToast() != null + } + assertEquals( + AppConfig.getInstance().resources.getString( + R.string.ssh_pem_key_parse_error, + AppConfig.getInstance().resources.getString( + R.string.ssh_key_no_decoder_decrypt + ) + ), + ShadowToast.getTextOfLatestToast() + ) + } + } + + private fun performTestInActivity(test: () -> Unit) { + scenario.onActivity { activity -> + AppConfig.getInstance().setMainActivityContext(activity) + test.invoke() + } + } +} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest.java deleted file mode 100644 index 3dfbaecf9b..0000000000 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest.java +++ /dev/null @@ -1,131 +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.asynchronous.asynctasks.ssh; - -import static org.junit.Assert.assertNotNull; - -import java.lang.reflect.Field; -import java.security.KeyPair; -import java.util.concurrent.ExecutionException; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import net.schmizz.sshj.userauth.password.PasswordFinder; -import net.schmizz.sshj.userauth.password.Resource; - -@RunWith(AndroidJUnit4.class) -public class PemToKeyPairTaskTest { - - private static final String unencryptedPuttyKey = - "PuTTY-User-Key-File-2: ssh-rsa\n" - + "Encryption: none\n" - + "Comment: Key for test only\n" - + "Public-Lines: 6\n" - + "AAAAB3NzaC1yc2EAAAABJQAAAQEA6ZhWkS0Xpb1riC5r3dulviIwVFUP4uXnnapv\n" - + "eqVwB/7aklhu3SnOlBwRMoan+AohhHogy2cjNMqW6x/xLwH9Cbo4kMTeJTPR7ca2\n" - + "lxmCtGgdFRheR84if6T+i2fb1ADUmncJEkL2H1Q7RG7+opoerDpwdGjopsP7s7H4\n" - + "ZTvGGXcudhrOFOf/gW8hR8m9wJ05ON8qfKiRWIKDxFpectFOpJC/NGP4F53EHNAk\n" - + "HIhNPSW5voytGvj4VaS/xRAs2HLmj7jTor/Le/vJlndmnyJkGIwEJbVpp5HsZG7R\n" - + "VyhqZwCcI6ZiMXvYSH6oplffUGZz5HXkskBmreMauZC1beN31Q==\n" - + "Private-Lines: 14\n" - + "AAABAD8iQOj3bizLaSu5hO/af9KFx99w74lukaA75sczobz4xXOpMrhQfQVvXpgI\n" - + "t8Z/R1Q8rurdms//Zw8dY8eD/zMPvELNbHjB5bXireOmB6ZhU/fdEo/yhd1PMAoA\n" - + "ZO0wqCm/To9QXjH7Fu/mpa9n7J1AOhGf0C0SX7QGlikyv+s0c4ib+ipR6TEoLRHD\n" - + "Oa85soSZGjPvPckkfSNncemEWuo4Hp0jiJOU/Gd37YNY4Jc9FhRRuBlTUvhBdg5g\n" - + "FsvHZD2fZ+5J0Z2Gm9tJL6Uq6rvNWVC9sFlnornPXM9/UvXQ1Q59rIk0CQNEOVr7\n" - + "kdYpYeUhYrrwhCVQrjbV8CxyRi0AAACBAP1T4/nXa/+tA9f3orMBkxzQF0FnPI/+\n" - + "e8YXvdHtkHl3/uJEEy9FmliJecKNtNBDN6Tu3iAs5ne/btvyduMgRAxqOyXxdQCq\n" - + "uR2iNoHLCDqgOUbXCh+swVHPXsdbbhv/54aWjLBbbfZ6S1CwTmPV3eAVJRb78JwS\n" - + "uBi8Sq/5ZnhFAAAAgQDsDyjp1Bm6nonVwaGCHRsH5JFuiHbP8cyIQzdnC6PrMFvS\n" - + "EU1PfMKSeudywnEKyljct7Njw5FnMt1InWj1X80adK/gWTmppAPRr0u0ipT4J9ry\n" - + "1yCj4zeNS/cylZZoP/rOCq8Z2mbzqIO28jN5e7xDMrutNdXhqrOwmMgM0AliUQAA\n" - + "AIEAjMoM3mw2YXE7U1X2H/hfYymMWC+6XU4XHCI2Fk+CWGUPxvDT3uqUtoEXOXkY\n" - + "THdPSgA2f6EmqCOPR1VAA4jdQkK8VkN3/O3zWFdfRGqN5Kka7a7cmcyd93sq3LIU\n" - + "EYe4EYW7BQwe1W5ZCO+lRzjquGAB5rMhdAnzYfvkPc7sfJ8=\n" - + "Private-MAC: 2cd5ec740c5dd854e8a6bea3773f98697670bdc6"; - - // Passphrase = test - private static final String encryptedPuttyKey = - "PuTTY-User-Key-File-2: ssh-rsa\n" - + "Encryption: aes256-cbc\n" - + "Comment: Key for test only\n" - + "Public-Lines: 6\n" - + "AAAAB3NzaC1yc2EAAAABJQAAAQEAoArpfCYeHImHcTELKVzVjyS6N6viAN4lzkWC\n" - + "EDCyBX6x4wwgVXRYQTbd6xNCpVb/TdBTN/aVF9EXtMW2TXyvntxGblE+ilK8b3GL\n" - + "zRfxjrjGsjqffwlHn3JaWpCOYtEqgGOLeKkofbKBXGn1aK6tvowPsY89Dl4aK857\n" - + "mwisAvCIxmd8b6f2aBy4MLQ7AdmZXxPq6YD9CDXPyQkNG4RH5RGAIAw7jD+O6tUo\n" - + "g69voQudjy3D51VQDGNxOJVojQiQvmRUR2qkSazPJqE/hFsdN6rKh+Pbe8h6z+rU\n" - + "bym37+sTK5JwWKHDQ7/ezLdNR38wAPHdz2VW5+0rqKm8LAtCGQ==\n" - + "Private-Lines: 14\n" - + "erS8mfUDEme+ujzF1k0GA7d2P4umHriMFQjBZIdvht6amZXoF1L+bkJp82/vG9lv\n" - + "YYNYQqk5tHezkV3sJncPwr3RI/0Y1L+WtKWSfE6OzSKdYJoX3WpAuMTeMlVrxu0t\n" - + "RXnjbfSz7Z1czryxO4NgAK3NsYQK6h290uq5/mpqP6fIhT3/tn+mH8kihAt8+uum\n" - + "1RW9ucNi3TZTG4I00Z3LWHw1VaqYFeCYh3yp8Canv3mKGn5ISqsd5ehNXW1TYvJF\n" - + "Bd742+JlxK8dhrAr2R+g+erSA0ac8Df3wH72syVPzdewnh+21zff3NGI9GWN799X\n" - + "CnVtf+psDPuebGQIHewNTGsaziNkAT5rGXdNMo3Xln/B1Wr9l8tIJAtDWSNqjDLp\n" - + "kAcLQ+Z1wTPZehZKBi0oTsLVm4tEcPQsbnuK9h+Y/d9EWcmBEiHTGM6otesNZNA8\n" - + "i247YZpyrG3azeRFBVMNSzKJ+vS2rKpgvm8nbYKy+nO5uNZDEm9oARr9QPCxdzXK\n" - + "dmI9F8IT3tLd4qCekD4DI9MKxJLjzFmyGOHc4zMxgUyL5BT5suVDIAWL/hiekwjt\n" - + "T1+V+TRScq4c+pIWxfVu4kY+HpLUpSR3RAVaRFar7jaB+YEEXw+gqEVTCyZeXGFf\n" - + "dWU+8BkhFBF24v3Qoi9SmuWYrSQGl9O8smIHW0H2JCNF+8oqpQG8dwx37L3VyMNq\n" - + "ApJh0LnRhoRwKo2YaAZKInaFTYS8Gnj7DvZ+l7lxPRfCV5yl9U2How2BI9YPRDDu\n" - + "Gs4agAG3InJnMiuIOzaNOIFLGM9STtYNyvG411rj6tR4EEQ6cJCxIlVe5a1mEt7M\n" - + "GVfbB5wUvow0o0a56OBmFMZOCxV2Vpxu6PuGTD8QQ0O0YzNDWFk3Fj2RRnnLCBLF\n" - + "Private-MAC: f742e2954fbb0c98984db0d9855a0f15507ecc0a"; - - @Test - public void testUnencryptedKeyToKeyPair() throws InterruptedException, ExecutionException { - PemToKeyPairTask task = new PemToKeyPairTask(unencryptedPuttyKey, result -> {}); - KeyPair result = task.execute().get(); - assertNotNull(result); - assertNotNull(result.getPublic()); - assertNotNull(result.getPrivate()); - } - - @Test - public void testEncryptedKeyToKeyPair() - throws InterruptedException, NoSuchFieldException, IllegalAccessException, - ExecutionException { - PemToKeyPairTask task = new PemToKeyPairTask(encryptedPuttyKey, result -> {}); - Field field = PemToKeyPairTask.class.getDeclaredField("passwordFinder"); - field.setAccessible(true); - field.set( - task, - new PasswordFinder() { - @Override - public char[] reqPassword(Resource resource) { - return "test".toCharArray(); - } - - @Override - public boolean shouldRetry(Resource resource) { - return false; - } - }); - KeyPair result = task.execute().get(); - assertNotNull(result); - assertNotNull(result.getPublic()); - assertNotNull(result.getPrivate()); - } -} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest2.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest2.java deleted file mode 100644 index a2cbad5176..0000000000 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairTaskTest2.java +++ /dev/null @@ -1,98 +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.asynchronous.asynctasks.ssh; - -import static org.junit.Assert.assertNotNull; - -import java.lang.reflect.Field; -import java.security.KeyPair; -import java.util.concurrent.ExecutionException; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import net.schmizz.sshj.userauth.password.PasswordFinder; -import net.schmizz.sshj.userauth.password.Resource; - -@RunWith(AndroidJUnit4.class) -public class PemToKeyPairTaskTest2 { - - // public key for authorized_keys: ssh-ed25519 - // AAAAC3NzaC1lZDI1NTE5AAAAIGxJHFewxU9tJn9hUq9e2C/+ELFw83NpmJ5NLFOzU7O3 test-openssh-key - private static final String unencryptedOpenSshKey = - "-----BEGIN OPENSSH PRIVATE KEY-----\n" - + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n" - + "QyNTUxOQAAACBsSRxXsMVPbSZ/YVKvXtgv/hCxcPNzaZieTSxTs1OztwAAAJhX2WUxV9ll\n" - + "MQAAAAtzc2gtZWQyNTUxOQAAACBsSRxXsMVPbSZ/YVKvXtgv/hCxcPNzaZieTSxTs1Oztw\n" - + "AAAECjSjwwMXPzbZWq/EBoA4HA9Lr7B1/Tw78K+k1zqAJwA2xJHFewxU9tJn9hUq9e2C/+\n" - + "ELFw83NpmJ5NLFOzU7O3AAAADmFpcndhdmVAaHN2MDEwAQIDBAUGBw==\n" - + "-----END OPENSSH PRIVATE KEY-----"; - - // Passphrase = 12345678 - // public key for authorized_keys: ssh-ed25519 - // AAAAC3NzaC1lZDI1NTE5AAAAIHio1/33U0XoewL1qGLmTzxyVNeYP5b0Tunv/SQrQi92 test-openssh-key - private static final String encryptedOpenSshKey = - "-----BEGIN OPENSSH PRIVATE KEY-----\n" - + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCwlfECA9\n" - + "+EGLwKVApTmomnAAAAZAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHio1/33U0XoewL1\n" - + "qGLmTzxyVNeYP5b0Tunv/SQrQi92AAAAoD2dysYInLaJgXIv6k/xFv7OblU9vWkCwcYnDW\n" - + "8Zj5+ke8QL2/r7EUBEvY1H02GenlEH1Ufct8ce7/eAWwd7aWukaSQXlKW9IBt5YrxW8+P/\n" - + "wrHcd/Z92eQ0E7NV6b6LnghGYlyCjpSBW+mxa0AAYPD21c95d/HvJF6zxQl/IKCCLdOrr/\n" - + "ilMCSIGQEdg71hA3MMZsRbUvazsnZTZXD9PLI=\n" - + "-----END OPENSSH PRIVATE KEY-----"; - - @Test - public void testUnencryptedKeyToKeyPair() throws ExecutionException, InterruptedException { - PemToKeyPairTask task = new PemToKeyPairTask(unencryptedOpenSshKey, result -> {}); - KeyPair result = task.execute().get(); - assertNotNull(result); - assertNotNull(result.getPublic()); - assertNotNull(result.getPrivate()); - } - - @Test - public void testEncryptedKeyToKeyPair() - throws InterruptedException, NoSuchFieldException, IllegalAccessException, - ExecutionException { - PemToKeyPairTask task = new PemToKeyPairTask(encryptedOpenSshKey, result -> {}); - Field field = PemToKeyPairTask.class.getDeclaredField("passwordFinder"); - field.setAccessible(true); - field.set( - task, - new PasswordFinder() { - @Override - public char[] reqPassword(Resource resource) { - return "12345678".toCharArray(); - } - - @Override - public boolean shouldRetry(Resource resource) { - return false; - } - }); - KeyPair result = task.execute().get(); - assertNotNull(result); - assertNotNull(result.getPublic()); - assertNotNull(result.getPrivate()); - } -} From c70ffad82580f395fa3d7a8b7dccce858188867d Mon Sep 17 00:00:00 2001 From: peerzadaburhan Date: Thu, 2 Feb 2023 11:38:35 +0530 Subject: [PATCH 042/384] Issue #3394 --- app/src/main/AndroidManifest.xml | 4 +- .../ui/activities/MainActivity.java | 4315 +++++++++-------- .../ui/views/appbar/SearchView.java | 1 + 3 files changed, 2180 insertions(+), 2140 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bd1b7c8b06..42e6e9ae77 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -62,7 +62,9 @@ android:launchMode="singleInstance" android:name=".ui.activities.MainActivity" android:theme="@style/appCompatBlack" - android:configChanges="uiMode" > + android:configChanges="uiMode" + android:windowSoftInputMode="adjustPan" + > 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 ff6c36469c..e1f509521e 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 @@ -162,6 +162,7 @@ import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Color; +import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.hardware.usb.UsbManager; import android.media.RingtoneManager; @@ -175,12 +176,19 @@ import android.os.storage.StorageVolume; import android.service.quicksettings.TileService; import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; 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.ViewTreeObserver; +import android.view.WindowInsets; import android.view.animation.DecelerateInterpolator; +import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import androidx.annotation.DrawableRes; @@ -191,6 +199,9 @@ import androidx.annotation.StringRes; import androidx.arch.core.util.Function; import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import androidx.loader.app.LoaderManager; @@ -205,7 +216,7 @@ import io.reactivex.schedulers.Schedulers; public class MainActivity extends PermissionsActivity - implements SmbConnectionListener, + implements SmbConnectionListener, BookmarkCallback, SearchWorkerFragment.HelperCallbacks, CloudConnectionCallbacks, @@ -213,788 +224,803 @@ public class MainActivity extends PermissionsActivity FolderChooserDialog.FolderCallback, PermissionsActivity.OnPermissionGranted { - private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); - - public static final Pattern DIR_SEPARATOR = Pattern.compile("/"); - public static final String TAG_ASYNC_HELPER = "async_helper"; - - private DataUtils dataUtils; - - public String path = ""; - public boolean mReturnIntent = false; - public boolean isCompressedOpen = false; - public boolean mRingtonePickerIntent = false; - public int skinStatusBar; - - private SpeedDialView floatingActionButton; - - public MainActivityHelper mainActivityHelper; - - public int operation = -1; - public ArrayList oparrayList; - public ArrayList> oparrayListList; - - // oppathe - the path at which certain operation needs to be performed - // oppathe1 - the new path which user wants to create/modify - // oppathList - the paths at which certain operation needs to be performed (pairs with - // oparrayList) - public String oppathe, oppathe1; - public ArrayList oppatheList; - - // This holds the Uris to be written at initFabToSave() - private ArrayList urisToBeSaved; - - public static final String PASTEHELPER_BUNDLE = "pasteHelper"; - - private static final String KEY_DRAWER_SELECTED = "selectitem"; - private static final String KEY_OPERATION_PATH = "oppathe"; - 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; - // private HistoryManager history, grid; - private MainActivity mainActivity = this; - private String pathInCompressedArchive; - private boolean openProcesses = false; - private MaterialDialog materialDialog; - private boolean backPressedToExitOnce = false; - private WeakReference toast = new WeakReference<>(null); - private Intent intent; - private View indicator_layout; + private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); + + public static final Pattern DIR_SEPARATOR = Pattern.compile("/"); + public static final String TAG_ASYNC_HELPER = "async_helper"; + + private DataUtils dataUtils; + + public String path = ""; + public boolean mReturnIntent = false; + public boolean isCompressedOpen = false; + public boolean mRingtonePickerIntent = false; + public int skinStatusBar; + + private SpeedDialView floatingActionButton; + + public MainActivityHelper mainActivityHelper; + + public int operation = -1; + public ArrayList oparrayList; + public ArrayList> oparrayListList; + + // oppathe - the path at which certain operation needs to be performed + // oppathe1 - the new path which user wants to create/modify + // oppathList - the paths at which certain operation needs to be performed (pairs with + // oparrayList) + public String oppathe, oppathe1; + public ArrayList oppatheList; + + // This holds the Uris to be written at initFabToSave() + private ArrayList urisToBeSaved; + + public static final String PASTEHELPER_BUNDLE = "pasteHelper"; + + private static final String KEY_DRAWER_SELECTED = "selectitem"; + private static final String KEY_OPERATION_PATH = "oppathe"; + 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; + // private HistoryManager history, grid; + private MainActivity mainActivity = this; + private String pathInCompressedArchive; + private boolean openProcesses = false; + private MaterialDialog materialDialog; + private boolean backPressedToExitOnce = false; + private WeakReference toast = new WeakReference<>(null); + private Intent intent; + private View indicator_layout; + + private AppBarLayout appBarLayout; + + private SpeedDialOverlayLayout fabBgView; + private UtilsHandler utilsHandler; + private CloudHandler cloudHandler; + private CloudLoaderAsyncTask cloudLoaderAsyncTask; + /** + * This is for a hack. + * + * @see MainActivity#onLoadFinished(Loader, Cursor) + */ + private Cursor cloudCursorData = null; - private AppBarLayout appBarLayout; + public static final int REQUEST_CODE_SAF = 223; - private SpeedDialOverlayLayout fabBgView; - private UtilsHandler utilsHandler; - private CloudHandler cloudHandler; - private CloudLoaderAsyncTask cloudLoaderAsyncTask; - /** - * This is for a hack. - * - * @see MainActivity#onLoadFinished(Loader, Cursor) - */ - private Cursor cloudCursorData = null; + public static final String KEY_INTENT_PROCESS_VIEWER = "openprocesses"; + public static final String TAG_INTENT_FILTER_FAILED_OPS = "failedOps"; + public static final String TAG_INTENT_FILTER_GENERAL = "general_communications"; + public static final String ARGS_KEY_LOADER = "loader_cloud_args_service"; - public static final int REQUEST_CODE_SAF = 223; + /** + * Broadcast which will be fired after every file operation, will denote list loading Registered + * by {@link MainFragment} + */ + public static final String KEY_INTENT_LOAD_LIST = "loadlist"; - public static final String KEY_INTENT_PROCESS_VIEWER = "openprocesses"; - public static final String TAG_INTENT_FILTER_FAILED_OPS = "failedOps"; - public static final String TAG_INTENT_FILTER_GENERAL = "general_communications"; - public static final String ARGS_KEY_LOADER = "loader_cloud_args_service"; + /** + * Extras carried by the list loading intent Contains path of parent directory in which operation + * took place, so that we can run media scanner on it + */ + public static final String KEY_INTENT_LOAD_LIST_FILE = "loadlist_file"; - /** - * Broadcast which will be fired after every file operation, will denote list loading Registered - * by {@link MainFragment} - */ - public static final String KEY_INTENT_LOAD_LIST = "loadlist"; + /** + * Mime type in intent that apps need to pass when trying to open file manager from a specific + * directory Should be clubbed with {@link Intent#ACTION_VIEW} and send in path to open in intent + * data field + */ + public static final String ARGS_INTENT_ACTION_VIEW_MIME_FOLDER = "resource/folder"; - /** - * Extras carried by the list loading intent Contains path of parent directory in which operation - * took place, so that we can run media scanner on it - */ - public static final String KEY_INTENT_LOAD_LIST_FILE = "loadlist_file"; + public static final String ARGS_INTENT_ACTION_VIEW_APPLICATION_ALL = "application/*"; - /** - * Mime type in intent that apps need to pass when trying to open file manager from a specific - * directory Should be clubbed with {@link Intent#ACTION_VIEW} and send in path to open in intent - * data field - */ - public static final String ARGS_INTENT_ACTION_VIEW_MIME_FOLDER = "resource/folder"; + public static final String CLOUD_AUTHENTICATOR_GDRIVE = "android.intent.category.BROWSABLE"; + public static final String CLOUD_AUTHENTICATOR_REDIRECT_URI = "com.amaze.filemanager:/auth"; - public static final String ARGS_INTENT_ACTION_VIEW_APPLICATION_ALL = "application/*"; + // the current visible tab, either 0 or 1 + public static int currentTab; + private boolean listItemSelected = false; - public static final String CLOUD_AUTHENTICATOR_GDRIVE = "android.intent.category.BROWSABLE"; - public static final String CLOUD_AUTHENTICATOR_REDIRECT_URI = "com.amaze.filemanager:/auth"; + private String scrollToFileName = null; - // the current visible tab, either 0 or 1 - public static int currentTab; - private boolean listItemSelected = false; + public static final int REQUEST_CODE_CLOUD_LIST_KEYS = 5463; + public static final int REQUEST_CODE_CLOUD_LIST_KEY = 5472; - private String scrollToFileName = null; + private PasteHelper pasteHelper; + private MainActivityActionMode mainActivityActionMode; - public static final int REQUEST_CODE_CLOUD_LIST_KEYS = 5463; - public static final int REQUEST_CODE_CLOUD_LIST_KEY = 5472; + private static final String DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0"; + private static final String INTERNAL_SHARED_STORAGE = "Internal shared storage"; + private static final String INTENT_ACTION_OPEN_QUICK_ACCESS = + "com.amaze.filemanager.openQuickAccess"; + private static final String INTENT_ACTION_OPEN_RECENT = "com.amaze.filemanager.openRecent"; + private static final String INTENT_ACTION_OPEN_FTP_SERVER = "com.amaze.filemanager.openFTPServer"; + private static final String INTENT_ACTION_OPEN_APP_MANAGER = + "com.amaze.filemanager.openAppManager"; - private PasteHelper pasteHelper; - private MainActivityActionMode mainActivityActionMode; - - private static final String DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0"; - private static final String INTERNAL_SHARED_STORAGE = "Internal shared storage"; - private static final String INTENT_ACTION_OPEN_QUICK_ACCESS = - "com.amaze.filemanager.openQuickAccess"; - private static final String INTENT_ACTION_OPEN_RECENT = "com.amaze.filemanager.openRecent"; - private static final String INTENT_ACTION_OPEN_FTP_SERVER = "com.amaze.filemanager.openFTPServer"; - private static final String INTENT_ACTION_OPEN_APP_MANAGER = - "com.amaze.filemanager.openAppManager"; - - /** Called when the activity is first created. */ - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main_toolbar); - - intent = getIntent(); - - dataUtils = DataUtils.getInstance(); - if (savedInstanceState != null) { - listItemSelected = savedInstanceState.getBoolean(KEY_SELECTED_LIST_ITEM, false); - } - - initialisePreferences(); - initializeInteractiveShell(); - - dataUtils.registerOnDataChangedListener(new SaveOnDataUtilsChange(drawer)); - - AppConfig.getInstance().setMainActivityContext(this); - - initialiseViews(); - utilsHandler = AppConfig.getInstance().getUtilsHandler(); - cloudHandler = new CloudHandler(this, AppConfig.getInstance().getExplorerDatabase()); - - initialiseFab(); // TODO: 7/12/2017 not init when actionIntent != null - mainActivityHelper = new MainActivityHelper(this); - mainActivityActionMode = new MainActivityActionMode(new WeakReference<>(MainActivity.this)); + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_toolbar); - if (CloudSheetFragment.isCloudProviderAvailable(this)) { - LoaderManager.getInstance(this).initLoader(REQUEST_CODE_CLOUD_LIST_KEYS, null, this); - } + intent = getIntent(); - path = intent.getStringExtra("path"); - openProcesses = intent.getBooleanExtra(KEY_INTENT_PROCESS_VIEWER, false); - if (intent.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { - ArrayList failedOps = - intent.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); - if (failedOps != null) { - mainActivityHelper.showFailedOperationDialog(failedOps, this); - } - } + dataUtils = DataUtils.getInstance(); + if (savedInstanceState != null) { + listItemSelected = savedInstanceState.getBoolean(KEY_SELECTED_LIST_ITEM, false); + } - checkForExternalIntent(intent); - - drawer.setDrawerIndicatorEnabled(); - - if (!getBoolean(PREFERENCE_BOOKMARKS_ADDED)) { - utilsHandler.addCommonBookmarks(); - getPrefs().edit().putBoolean(PREFERENCE_BOOKMARKS_ADDED, true).commit(); - } - - checkForExternalPermission(); - - Completable.fromRunnable( - () -> { - dataUtils.setHiddenFiles(utilsHandler.getHiddenFilesConcurrentRadixTree()); - dataUtils.setHistory(utilsHandler.getHistoryLinkedList()); - dataUtils.setGridfiles(utilsHandler.getGridViewList()); - dataUtils.setListfiles(utilsHandler.getListViewList()); - dataUtils.setBooks(utilsHandler.getBookmarksList()); - ArrayList servers = new ArrayList<>(); - servers.addAll(utilsHandler.getSmbList()); - servers.addAll(utilsHandler.getSftpList()); - dataUtils.setServers(servers); - - ExtensionsKt.updateAUAlias( - this, - !PackageUtils.Companion.appInstalledOrNot( - AboutActivity.PACKAGE_AMAZE_UTILS, mainActivity.getPackageManager()) - && !getBoolean( - PreferencesConstants.PREFERENCE_DISABLE_PLAYER_INTENT_FILTERS)); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - new CompletableObserver() { - @Override - public void onSubscribe(@NonNull Disposable d) {} - - @Override - public void onComplete() { - drawer.refreshDrawer(); - invalidateFragmentAndBundle(savedInstanceState, false); - } - - @Override - public void onError(@NonNull Throwable e) { - LOG.error("Error setting up DataUtils", e); - drawer.refreshDrawer(); - invalidateFragmentAndBundle(savedInstanceState, false); - } - }); - initStatusBarResources(findViewById(R.id.drawer_layout)); - } - - public void invalidateFragmentAndBundle(Bundle savedInstanceState, boolean isCloudRefresh) { - if (savedInstanceState == null) { - if (openProcesses) { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace( - R.id.content_frame, new ProcessViewerFragment(), KEY_INTENT_PROCESS_VIEWER); - // transaction.addToBackStack(null); - openProcesses = false; - // title.setText(utils.getString(con, R.string.process_viewer)); - // Commit the transaction - transaction.commit(); - supportInvalidateOptionsMenu(); - } else if (intent.getAction() != null - && (intent.getAction().equals(TileService.ACTION_QS_TILE_PREFERENCES) - || INTENT_ACTION_OPEN_FTP_SERVER.equals(intent.getAction()))) { - // tile preferences, open ftp fragment - - FragmentTransaction transaction2 = getSupportFragmentManager().beginTransaction(); - transaction2.replace(R.id.content_frame, new FtpServerFragment()); - appBarLayout - .animate() - .translationY(0) - .setInterpolator(new DecelerateInterpolator(2)) - .start(); - - drawer.deselectEverything(); - transaction2.commit(); - } else if (intent.getAction() != null - && INTENT_ACTION_OPEN_APP_MANAGER.equals(intent.getAction())) { - FragmentTransaction transaction3 = getSupportFragmentManager().beginTransaction(); - transaction3.replace(R.id.content_frame, new AppsListFragment()); - appBarLayout - .animate() - .translationY(0) - .setInterpolator(new DecelerateInterpolator(2)) - .start(); - - drawer.deselectEverything(); - transaction3.commit(); - } else { - if (path != null && path.length() > 0) { - HybridFile file = new HybridFile(OpenMode.UNKNOWN, path); - file.generateMode(MainActivity.this); - if (file.isCloudDriveFile() && dataUtils.getAccounts().size() == 0) { - // not ready to serve cloud files - goToMain(null); - } else if (file.isDirectory(MainActivity.this) && !isCloudRefresh) { - goToMain(path); - } else { - if (!isCloudRefresh) { - goToMain(null); - } - if (file.isSmb() || file.isSftp()) { - String authorisedPath = - SshClientUtils.formatPlainServerPathToAuthorised(dataUtils.getServers(), path); - file.setPath(authorisedPath); - LOG.info( - "Opening smb file from deeplink, modify plain path to authorised path {}", - authorisedPath); + initialisePreferences(); + initializeInteractiveShell(); + + + dataUtils.registerOnDataChangedListener(new SaveOnDataUtilsChange(drawer)); + + AppConfig.getInstance().setMainActivityContext(this); + + initialiseViews(); + utilsHandler = AppConfig.getInstance().getUtilsHandler(); + cloudHandler = new CloudHandler(this, AppConfig.getInstance().getExplorerDatabase()); + + initialiseFab(); // TODO: 7/12/2017 not init when actionIntent != null + mainActivityHelper = new MainActivityHelper(this); + mainActivityActionMode = new MainActivityActionMode(new WeakReference<>(MainActivity.this)); + + if (CloudSheetFragment.isCloudProviderAvailable(this)) { + + LoaderManager.getInstance(this).initLoader(REQUEST_CODE_CLOUD_LIST_KEYS, null, this); + } + + path = intent.getStringExtra("path"); + openProcesses = intent.getBooleanExtra(KEY_INTENT_PROCESS_VIEWER, false); + + if (intent.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { + ArrayList failedOps = + intent.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); + if (failedOps != null) { + mainActivityHelper.showFailedOperationDialog(failedOps, this); } - file.openFile(this, true); - } - } else if (!isCloudRefresh) { - goToMain(null); } - } - } else { - pasteHelper = savedInstanceState.getParcelable(PASTEHELPER_BUNDLE); - oppathe = savedInstanceState.getString(KEY_OPERATION_PATH); - oppathe1 = savedInstanceState.getString(KEY_OPERATED_ON_PATH); - oparrayList = savedInstanceState.getParcelableArrayList(KEY_OPERATIONS_PATH_LIST); - operation = savedInstanceState.getInt(KEY_OPERATION); - int selectedStorage = savedInstanceState.getInt(KEY_DRAWER_SELECTED, 0); - getDrawer().selectCorrectDrawerItem(selectedStorage); - } - } - - @Override - public void onPermissionGranted() { - drawer.refreshDrawer(); - TabFragment tabFragment = getTabFragment(); - boolean b = getBoolean(PREFERENCE_NEED_TO_SET_HOME); - // reset home and current paths according to new storages - if (b) { - TabHandler tabHandler = TabHandler.getInstance(); - tabHandler - .clear() - .subscribe( - () -> { - if (tabFragment != null) { - tabFragment.refactorDrawerStorages(false); - Fragment main = tabFragment.getFragmentAtIndex(0); - if (main != null) ((MainFragment) main).updateTabWithDb(tabHandler.findTab(1)); - Fragment main1 = tabFragment.getFragmentAtIndex(1); - if (main1 != null) ((MainFragment) main1).updateTabWithDb(tabHandler.findTab(2)); + + checkForExternalIntent(intent); + + drawer.setDrawerIndicatorEnabled(); + + + if (!getBoolean(PREFERENCE_BOOKMARKS_ADDED)) { + utilsHandler.addCommonBookmarks(); + getPrefs().edit().putBoolean(PREFERENCE_BOOKMARKS_ADDED, true).commit(); + } + + checkForExternalPermission(); + + Completable.fromRunnable( + () -> { + dataUtils.setHiddenFiles(utilsHandler.getHiddenFilesConcurrentRadixTree()); + dataUtils.setHistory(utilsHandler.getHistoryLinkedList()); + dataUtils.setGridfiles(utilsHandler.getGridViewList()); + dataUtils.setListfiles(utilsHandler.getListViewList()); + dataUtils.setBooks(utilsHandler.getBookmarksList()); + ArrayList servers = new ArrayList<>(); + servers.addAll(utilsHandler.getSmbList()); + servers.addAll(utilsHandler.getSftpList()); + dataUtils.setServers(servers); + + ExtensionsKt.updateAUAlias( + this, + !PackageUtils.Companion.appInstalledOrNot( + AboutActivity.PACKAGE_AMAZE_UTILS, mainActivity.getPackageManager()) + && !getBoolean( + PreferencesConstants.PREFERENCE_DISABLE_PLAYER_INTENT_FILTERS)); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + new CompletableObserver() { + @Override + public void onSubscribe(@NonNull Disposable d) { + } + + @Override + public void onComplete() { + drawer.refreshDrawer(); + invalidateFragmentAndBundle(savedInstanceState, false); + } + + @Override + public void onError(@NonNull Throwable e) { + LOG.error("Error setting up DataUtils", e); + drawer.refreshDrawer(); + invalidateFragmentAndBundle(savedInstanceState, false); + } + }); + initStatusBarResources(findViewById(R.id.drawer_layout)); + } + + public void invalidateFragmentAndBundle(Bundle savedInstanceState, boolean isCloudRefresh) { + if (savedInstanceState == null) { + if (openProcesses) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace( + R.id.content_frame, new ProcessViewerFragment(), KEY_INTENT_PROCESS_VIEWER); + // transaction.addToBackStack(null); + openProcesses = false; + // title.setText(utils.getString(con, R.string.process_viewer)); + // Commit the transaction + transaction.commit(); + supportInvalidateOptionsMenu(); + } else if (intent.getAction() != null + && (intent.getAction().equals(TileService.ACTION_QS_TILE_PREFERENCES) + || INTENT_ACTION_OPEN_FTP_SERVER.equals(intent.getAction()))) { + // tile preferences, open ftp fragment + + FragmentTransaction transaction2 = getSupportFragmentManager().beginTransaction(); + transaction2.replace(R.id.content_frame, new FtpServerFragment()); + appBarLayout + .animate() + .translationY(0) + .setInterpolator(new DecelerateInterpolator(2)) + .start(); + + drawer.deselectEverything(); + transaction2.commit(); + } else if (intent.getAction() != null + && INTENT_ACTION_OPEN_APP_MANAGER.equals(intent.getAction())) { + FragmentTransaction transaction3 = getSupportFragmentManager().beginTransaction(); + transaction3.replace(R.id.content_frame, new AppsListFragment()); + appBarLayout + .animate() + .translationY(0) + .setInterpolator(new DecelerateInterpolator(2)) + .start(); + + drawer.deselectEverything(); + transaction3.commit(); + } else { + if (path != null && path.length() > 0) { + HybridFile file = new HybridFile(OpenMode.UNKNOWN, path); + file.generateMode(MainActivity.this); + if (file.isCloudDriveFile() && dataUtils.getAccounts().size() == 0) { + // not ready to serve cloud files + goToMain(null); + } else if (file.isDirectory(MainActivity.this) && !isCloudRefresh) { + goToMain(path); + } else { + if (!isCloudRefresh) { + goToMain(null); + } + if (file.isSmb() || file.isSftp()) { + String authorisedPath = + SshClientUtils.formatPlainServerPathToAuthorised(dataUtils.getServers(), path); + file.setPath(authorisedPath); + LOG.info( + "Opening smb file from deeplink, modify plain path to authorised path {}", + authorisedPath); + } + file.openFile(this, true); + } + } else if (!isCloudRefresh) { + goToMain(null); } - getPrefs().edit().putBoolean(PREFERENCE_NEED_TO_SET_HOME, false).commit(); - }); - } else { - // just refresh list - if (tabFragment != null) { - Fragment main = tabFragment.getFragmentAtIndex(0); - if (main != null) ((MainFragment) main).updateList(false); - Fragment main1 = tabFragment.getFragmentAtIndex(1); - if (main1 != null) ((MainFragment) main1).updateList(false); - } + } + } else { + pasteHelper = savedInstanceState.getParcelable(PASTEHELPER_BUNDLE); + oppathe = savedInstanceState.getString(KEY_OPERATION_PATH); + oppathe1 = savedInstanceState.getString(KEY_OPERATED_ON_PATH); + oparrayList = savedInstanceState.getParcelableArrayList(KEY_OPERATIONS_PATH_LIST); + operation = savedInstanceState.getInt(KEY_OPERATION); + int selectedStorage = savedInstanceState.getInt(KEY_DRAWER_SELECTED, 0); + getDrawer().selectCorrectDrawerItem(selectedStorage); + } } - } - private void checkForExternalPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (!checkStoragePermission()) { - requestStoragePermission(this, true); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - requestAllFilesAccess(this); - } + @Override + public void onPermissionGranted() { + drawer.refreshDrawer(); + TabFragment tabFragment = getTabFragment(); + boolean b = getBoolean(PREFERENCE_NEED_TO_SET_HOME); + // reset home and current paths according to new storages + if (b) { + TabHandler tabHandler = TabHandler.getInstance(); + tabHandler + .clear() + .subscribe( + () -> { + if (tabFragment != null) { + tabFragment.refactorDrawerStorages(false); + Fragment main = tabFragment.getFragmentAtIndex(0); + if (main != null) + ((MainFragment) main).updateTabWithDb(tabHandler.findTab(1)); + Fragment main1 = tabFragment.getFragmentAtIndex(1); + if (main1 != null) + ((MainFragment) main1).updateTabWithDb(tabHandler.findTab(2)); + } + getPrefs().edit().putBoolean(PREFERENCE_NEED_TO_SET_HOME, false).commit(); + }); + } else { + // just refresh list + if (tabFragment != null) { + Fragment main = tabFragment.getFragmentAtIndex(0); + if (main != null) ((MainFragment) main).updateList(false); + Fragment main1 = tabFragment.getFragmentAtIndex(1); + if (main1 != null) ((MainFragment) main1).updateList(false); + } + } } - } - /** Checks for the action to take when Amaze receives an intent from external source */ - private void checkForExternalIntent(Intent intent) { - final String actionIntent = intent.getAction(); - if (actionIntent == null) { - return; + private void checkForExternalPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (!checkStoragePermission()) { + requestStoragePermission(this, true); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + requestAllFilesAccess(this); + } + } } - final String type = intent.getType(); + /** + * Checks for the action to take when Amaze receives an intent from external source + */ + private void checkForExternalIntent(Intent intent) { + final String actionIntent = intent.getAction(); + if (actionIntent == null) { + return; + } - if (actionIntent.equals(Intent.ACTION_GET_CONTENT)) { - // file picker intent - mReturnIntent = true; - Toast.makeText(this, getString(R.string.pick_a_file), Toast.LENGTH_LONG).show(); + final String type = intent.getType(); + + if (actionIntent.equals(Intent.ACTION_GET_CONTENT)) { + // file picker intent + mReturnIntent = true; + Toast.makeText(this, getString(R.string.pick_a_file), Toast.LENGTH_LONG).show(); + + // disable screen rotation just for convenience purpose + // TODO: Support screen rotation when picking file + Utils.disableScreenRotation(this); + } else if (actionIntent.equals(RingtoneManager.ACTION_RINGTONE_PICKER)) { + // ringtone picker intent + mReturnIntent = true; + mRingtonePickerIntent = true; + Toast.makeText(this, getString(R.string.pick_a_file), Toast.LENGTH_LONG).show(); + + // disable screen rotation just for convenience purpose + // TODO: Support screen rotation when picking file + Utils.disableScreenRotation(this); + } else if (actionIntent.equals(Intent.ACTION_VIEW)) { + // zip viewer intent + Uri uri = intent.getData(); + + if (type != null + && (type.equals(ARGS_INTENT_ACTION_VIEW_MIME_FOLDER) + || type.equals(ARGS_INTENT_ACTION_VIEW_APPLICATION_ALL))) { + // support for syncting or intents from external apps that + // need to start file manager from a specific path + + if (uri != null) { + + path = Utils.sanitizeInput(FileUtils.fromContentUri(uri).getAbsolutePath()); + scrollToFileName = intent.getStringExtra("com.amaze.fileutilities.AFM_LOCATE_FILE_NAME"); + } else { + // no data field, open home for the tab in later processing + path = null; + } + } else if (FileUtils.isCompressedFile(Utils.sanitizeInput(uri.toString()))) { + // we don't have folder resource mime type set, supposed to be zip/rar + isCompressedOpen = true; + pathInCompressedArchive = Utils.sanitizeInput(uri.toString()); + openCompressed(pathInCompressedArchive); + } else if (uri.getPath().startsWith("/open_file")) { + /** + * Deeplink to open files directly through amaze using following format: + * http://teamamaze.xyz/open_file?path=path-to-file + */ + path = Utils.sanitizeInput(uri.getQueryParameter("path")); + } else { + LOG.warn(getString(R.string.error_cannot_find_way_open)); + } - // disable screen rotation just for convenience purpose - // TODO: Support screen rotation when picking file - Utils.disableScreenRotation(this); - } else if (actionIntent.equals(RingtoneManager.ACTION_RINGTONE_PICKER)) { - // ringtone picker intent - mReturnIntent = true; - mRingtonePickerIntent = true; - Toast.makeText(this, getString(R.string.pick_a_file), Toast.LENGTH_LONG).show(); + } else if (actionIntent.equals(Intent.ACTION_SEND)) { + if ("text/plain".equals(type)) { + initFabToSave(null); + } else { + // save a single file to filesystem + Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + ArrayList uris = new ArrayList<>(); + uris.add(uri); + initFabToSave(uris); + } + // disable screen rotation just for convenience purpose + // TODO: Support screen rotation when saving a file + Utils.disableScreenRotation(this); + + } else if (actionIntent.equals(Intent.ACTION_SEND_MULTIPLE) && type != null) { + // save multiple files to filesystem + + ArrayList arrayList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + initFabToSave(arrayList); - // disable screen rotation just for convenience purpose - // TODO: Support screen rotation when picking file - Utils.disableScreenRotation(this); - } else if (actionIntent.equals(Intent.ACTION_VIEW)) { - // zip viewer intent - Uri uri = intent.getData(); + // disable screen rotation just for convenience purpose + // TODO: Support screen rotation when saving a file + Utils.disableScreenRotation(this); + } + } + + /** + * Initializes the floating action button to act as to save data from an external intent + */ + private void initFabToSave(final ArrayList uris) { + Utils.showThemedSnackbar( + this, + getString(R.string.select_save_location), + BaseTransientBottomBar.LENGTH_INDEFINITE, + R.string.save, + () -> saveExternalIntent(uris)); + } - if (type != null - && (type.equals(ARGS_INTENT_ACTION_VIEW_MIME_FOLDER) - || type.equals(ARGS_INTENT_ACTION_VIEW_APPLICATION_ALL))) { - // support for syncting or intents from external apps that - // need to start file manager from a specific path + private void saveExternalIntent(final ArrayList uris) { + executeWithMainFragment( + mainFragment -> { + if (uris != null && uris.size() > 0) { + if (SDK_INT >= LOLLIPOP) { + File folder = new File(mainFragment.getCurrentPath()); + int result = mainActivityHelper.checkFolder(folder, MainActivity.this); + if (result == WRITABLE_OR_ON_SDCARD) { + FileUtil.writeUriToStorage( + MainActivity.this, uris, getContentResolver(), mainFragment.getCurrentPath()); + finish(); + } else { + // Trigger SAF intent, keep uri until finish + operation = SAVE_FILE; + urisToBeSaved = uris; + mainActivityHelper.checkFolder(folder, MainActivity.this); + } + } else { + FileUtil.writeUriToStorage( + MainActivity.this, uris, getContentResolver(), mainFragment.getCurrentPath()); + } + } else { + saveExternalIntentExtras(); + } + Toast.makeText( + MainActivity.this, + getResources().getString(R.string.saving) + + " to " + + mainFragment.getCurrentPath(), + Toast.LENGTH_LONG) + .show(); + finish(); + return null; + }); + } - if (uri != null) { + private void saveExternalIntentExtras() { + executeWithMainFragment( + mainFragment -> { + Bundle extras = intent.getExtras(); + StringBuilder data = new StringBuilder(); + if (!Utils.isNullOrEmpty(extras.getString(Intent.EXTRA_SUBJECT))) { + data.append(extras.getString(Intent.EXTRA_SUBJECT)); + } + if (!Utils.isNullOrEmpty(extras.getString(Intent.EXTRA_TEXT))) { + data.append(AppConstants.NEW_LINE).append(extras.getString(Intent.EXTRA_TEXT)); + } + String fileName = Long.toString(System.currentTimeMillis()); + AppConfig.getInstance() + .runInBackground( + () -> + MakeFileOperation.mktextfile( + data.toString(), mainFragment.getCurrentPath(), fileName)); + return null; + }); + } - path = Utils.sanitizeInput(FileUtils.fromContentUri(uri).getAbsolutePath()); - scrollToFileName = intent.getStringExtra("com.amaze.fileutilities.AFM_LOCATE_FILE_NAME"); + public void clearFabActionItems() { + floatingActionButton.removeActionItemById(R.id.menu_new_folder); + floatingActionButton.removeActionItemById(R.id.menu_new_file); + floatingActionButton.removeActionItemById(R.id.menu_new_cloud); + } + + /** + * Initializes an interactive shell, which will stay throughout the app lifecycle. + */ + private void initializeInteractiveShell() { + if (isRootExplorer()) { + // Enable mount-master flag when invoking su command, to force su run in the global mount + // namespace. See https://github.com/topjohnwu/libsu/issues/75 + Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)); + Shell.getShell(); + } + } + + /** + * @return paths to all available volumes in the system (include emulated) + */ + public synchronized ArrayList getStorageDirectories() { + ArrayList volumes; + if (SDK_INT >= N) { + volumes = getStorageDirectoriesNew(); } else { - // no data field, open home for the tab in later processing - path = null; - } - } else if (FileUtils.isCompressedFile(Utils.sanitizeInput(uri.toString()))) { - // we don't have folder resource mime type set, supposed to be zip/rar - isCompressedOpen = true; - pathInCompressedArchive = Utils.sanitizeInput(uri.toString()); - openCompressed(pathInCompressedArchive); - } else if (uri.getPath().startsWith("/open_file")) { - /** - * Deeplink to open files directly through amaze using following format: - * http://teamamaze.xyz/open_file?path=path-to-file - */ - path = Utils.sanitizeInput(uri.getQueryParameter("path")); - } else { - LOG.warn(getString(R.string.error_cannot_find_way_open)); - } + volumes = getStorageDirectoriesLegacy(); + } + if (isRootExplorer()) { + volumes.add( + new StorageDirectoryParcelable( + "/", + getResources().getString(R.string.root_directory), + R.drawable.ic_drawer_root_white)); + } + return volumes; + } - } else if (actionIntent.equals(Intent.ACTION_SEND)) { - if ("text/plain".equals(type)) { - initFabToSave(null); - } else { - // save a single file to filesystem - Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - ArrayList uris = new ArrayList<>(); - uris.add(uri); - initFabToSave(uris); - } - // disable screen rotation just for convenience purpose - // TODO: Support screen rotation when saving a file - Utils.disableScreenRotation(this); - - } else if (actionIntent.equals(Intent.ACTION_SEND_MULTIPLE) && type != null) { - // save multiple files to filesystem - - ArrayList arrayList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - initFabToSave(arrayList); - - // disable screen rotation just for convenience purpose - // TODO: Support screen rotation when saving a file - Utils.disableScreenRotation(this); - } - } - - /** Initializes the floating action button to act as to save data from an external intent */ - private void initFabToSave(final ArrayList uris) { - Utils.showThemedSnackbar( - this, - getString(R.string.select_save_location), - BaseTransientBottomBar.LENGTH_INDEFINITE, - R.string.save, - () -> saveExternalIntent(uris)); - } - - private void saveExternalIntent(final ArrayList uris) { - executeWithMainFragment( - mainFragment -> { - if (uris != null && uris.size() > 0) { - if (SDK_INT >= LOLLIPOP) { - File folder = new File(mainFragment.getCurrentPath()); - int result = mainActivityHelper.checkFolder(folder, MainActivity.this); - if (result == WRITABLE_OR_ON_SDCARD) { - FileUtil.writeUriToStorage( - MainActivity.this, uris, getContentResolver(), mainFragment.getCurrentPath()); - finish(); - } else { - // Trigger SAF intent, keep uri until finish - operation = SAVE_FILE; - urisToBeSaved = uris; - mainActivityHelper.checkFolder(folder, MainActivity.this); - } + /** + * @return All available storage volumes (including internal storage, SD-Cards and USB devices) + */ + @TargetApi(N) + public synchronized ArrayList getStorageDirectoriesNew() { + // Final set of paths + ArrayList volumes = new ArrayList<>(); + StorageManager sm = getSystemService(StorageManager.class); + for (StorageVolume volume : sm.getStorageVolumes()) { + if (!volume.getState().equalsIgnoreCase(Environment.MEDIA_MOUNTED) + && !volume.getState().equalsIgnoreCase(Environment.MEDIA_MOUNTED_READ_ONLY)) { + continue; + } + File path = Utils.getVolumeDirectory(volume); + String name = volume.getDescription(this); + if (INTERNAL_SHARED_STORAGE.equalsIgnoreCase(name)) { + name = getString(R.string.storage_internal); + } + int icon; + if (!volume.isRemovable()) { + icon = R.drawable.ic_phone_android_white_24dp; } else { - FileUtil.writeUriToStorage( - MainActivity.this, uris, getContentResolver(), mainFragment.getCurrentPath()); + // HACK: There is no reliable way to distinguish USB and SD external storage + // However it is often enough to check for "USB" String + if (name.toUpperCase().contains("USB") || path.getPath().toUpperCase().contains("USB")) { + icon = R.drawable.ic_usb_white_24dp; + } else { + icon = R.drawable.ic_sd_storage_white_24dp; + } } - } else { - saveExternalIntentExtras(); - } - Toast.makeText( - MainActivity.this, - getResources().getString(R.string.saving) - + " to " - + mainFragment.getCurrentPath(), - Toast.LENGTH_LONG) - .show(); - finish(); - return null; - }); - } - - private void saveExternalIntentExtras() { - executeWithMainFragment( - mainFragment -> { - Bundle extras = intent.getExtras(); - StringBuilder data = new StringBuilder(); - if (!Utils.isNullOrEmpty(extras.getString(Intent.EXTRA_SUBJECT))) { - data.append(extras.getString(Intent.EXTRA_SUBJECT)); - } - if (!Utils.isNullOrEmpty(extras.getString(Intent.EXTRA_TEXT))) { - data.append(AppConstants.NEW_LINE).append(extras.getString(Intent.EXTRA_TEXT)); - } - String fileName = Long.toString(System.currentTimeMillis()); - AppConfig.getInstance() - .runInBackground( - () -> - MakeFileOperation.mktextfile( - data.toString(), mainFragment.getCurrentPath(), fileName)); - return null; - }); - } - - public void clearFabActionItems() { - floatingActionButton.removeActionItemById(R.id.menu_new_folder); - floatingActionButton.removeActionItemById(R.id.menu_new_file); - floatingActionButton.removeActionItemById(R.id.menu_new_cloud); - } - - /** Initializes an interactive shell, which will stay throughout the app lifecycle. */ - private void initializeInteractiveShell() { - if (isRootExplorer()) { - // Enable mount-master flag when invoking su command, to force su run in the global mount - // namespace. See https://github.com/topjohnwu/libsu/issues/75 - Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)); - Shell.getShell(); - } - } - - /** - * @return paths to all available volumes in the system (include emulated) - */ - public synchronized ArrayList getStorageDirectories() { - ArrayList volumes; - if (SDK_INT >= N) { - volumes = getStorageDirectoriesNew(); - } else { - volumes = getStorageDirectoriesLegacy(); - } - if (isRootExplorer()) { - volumes.add( - new StorageDirectoryParcelable( - "/", - getResources().getString(R.string.root_directory), - R.drawable.ic_drawer_root_white)); - } - return volumes; - } - - /** - * @return All available storage volumes (including internal storage, SD-Cards and USB devices) - */ - @TargetApi(N) - public synchronized ArrayList getStorageDirectoriesNew() { - // Final set of paths - ArrayList volumes = new ArrayList<>(); - StorageManager sm = getSystemService(StorageManager.class); - for (StorageVolume volume : sm.getStorageVolumes()) { - if (!volume.getState().equalsIgnoreCase(Environment.MEDIA_MOUNTED) - && !volume.getState().equalsIgnoreCase(Environment.MEDIA_MOUNTED_READ_ONLY)) { - continue; - } - File path = Utils.getVolumeDirectory(volume); - String name = volume.getDescription(this); - if (INTERNAL_SHARED_STORAGE.equalsIgnoreCase(name)) { - name = getString(R.string.storage_internal); - } - int icon; - if (!volume.isRemovable()) { - icon = R.drawable.ic_phone_android_white_24dp; - } else { - // HACK: There is no reliable way to distinguish USB and SD external storage - // However it is often enough to check for "USB" String - if (name.toUpperCase().contains("USB") || path.getPath().toUpperCase().contains("USB")) { - icon = R.drawable.ic_usb_white_24dp; - } else { - icon = R.drawable.ic_sd_storage_white_24dp; + volumes.add(new StorageDirectoryParcelable(path.getPath(), name, icon)); } - } - volumes.add(new StorageDirectoryParcelable(path.getPath(), name, icon)); - } - return volumes; - } - - /** - * Returns all available SD-Cards in the system (include emulated) - * - *

Warning: Hack! Based on Android source code of version 4.3 (API 18) Because there was no - * standard way to get it before android N - * - * @return All available SD-Cards in the system (include emulated) - */ - public synchronized ArrayList getStorageDirectoriesLegacy() { - List rv = new ArrayList<>(); - - // Primary physical SD-CARD (not emulated) - final String rawExternalStorage = System.getenv("EXTERNAL_STORAGE"); - // All Secondary SD-CARDs (all exclude primary) separated by ":" - final String rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE"); - // Primary emulated SD-CARD - final String rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET"); - if (TextUtils.isEmpty(rawEmulatedStorageTarget)) { - // Device has physical external storage; use plain paths. - if (TextUtils.isEmpty(rawExternalStorage)) { - // EXTERNAL_STORAGE undefined; falling back to default. - // Check for actual existence of the directory before adding to list - if (new File(DEFAULT_FALLBACK_STORAGE_PATH).exists()) { - rv.add(DEFAULT_FALLBACK_STORAGE_PATH); + return volumes; + } + + /** + * Returns all available SD-Cards in the system (include emulated) + * + *

Warning: Hack! Based on Android source code of version 4.3 (API 18) Because there was no + * standard way to get it before android N + * + * @return All available SD-Cards in the system (include emulated) + */ + public synchronized ArrayList getStorageDirectoriesLegacy() { + List rv = new ArrayList<>(); + + // Primary physical SD-CARD (not emulated) + final String rawExternalStorage = System.getenv("EXTERNAL_STORAGE"); + // All Secondary SD-CARDs (all exclude primary) separated by ":" + final String rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE"); + // Primary emulated SD-CARD + final String rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET"); + if (TextUtils.isEmpty(rawEmulatedStorageTarget)) { + // Device has physical external storage; use plain paths. + if (TextUtils.isEmpty(rawExternalStorage)) { + // EXTERNAL_STORAGE undefined; falling back to default. + // Check for actual existence of the directory before adding to list + if (new File(DEFAULT_FALLBACK_STORAGE_PATH).exists()) { + rv.add(DEFAULT_FALLBACK_STORAGE_PATH); + } else { + // We know nothing else, use Environment's fallback + rv.add(Environment.getExternalStorageDirectory().getAbsolutePath()); + } + } else { + rv.add(rawExternalStorage); + } } else { - // We know nothing else, use Environment's fallback - rv.add(Environment.getExternalStorageDirectory().getAbsolutePath()); + // Device has emulated storage; external storage paths should have + // userId burned into them. + final String rawUserId; + if (SDK_INT < JELLY_BEAN_MR1) { + rawUserId = ""; + } else { + final String path = Environment.getExternalStorageDirectory().getAbsolutePath(); + final String[] folders = DIR_SEPARATOR.split(path); + final String lastFolder = folders[folders.length - 1]; + boolean isDigit = false; + try { + Integer.valueOf(lastFolder); + isDigit = true; + } catch (NumberFormatException ignored) { + } + rawUserId = isDigit ? lastFolder : ""; + } + // /storage/emulated/0[1,2,...] + if (TextUtils.isEmpty(rawUserId)) { + rv.add(rawEmulatedStorageTarget); + } else { + rv.add(rawEmulatedStorageTarget + File.separator + rawUserId); + } } - } else { - rv.add(rawExternalStorage); - } - } else { - // Device has emulated storage; external storage paths should have - // userId burned into them. - final String rawUserId; - if (SDK_INT < JELLY_BEAN_MR1) { - rawUserId = ""; - } else { - final String path = Environment.getExternalStorageDirectory().getAbsolutePath(); - final String[] folders = DIR_SEPARATOR.split(path); - final String lastFolder = folders[folders.length - 1]; - boolean isDigit = false; - try { - Integer.valueOf(lastFolder); - isDigit = true; - } catch (NumberFormatException ignored) { + // Add all secondary storages + if (!TextUtils.isEmpty(rawSecondaryStoragesStr)) { + // All Secondary SD-CARDs splited into array + final String[] rawSecondaryStorages = rawSecondaryStoragesStr.split(File.pathSeparator); + Collections.addAll(rv, rawSecondaryStorages); } - rawUserId = isDigit ? lastFolder : ""; - } - // /storage/emulated/0[1,2,...] - if (TextUtils.isEmpty(rawUserId)) { - rv.add(rawEmulatedStorageTarget); - } else { - rv.add(rawEmulatedStorageTarget + File.separator + rawUserId); - } - } - // Add all secondary storages - if (!TextUtils.isEmpty(rawSecondaryStoragesStr)) { - // All Secondary SD-CARDs splited into array - final String[] rawSecondaryStorages = rawSecondaryStoragesStr.split(File.pathSeparator); - Collections.addAll(rv, rawSecondaryStorages); - } - if (SDK_INT >= M && checkStoragePermission()) rv.clear(); - if (SDK_INT >= KITKAT) { - String strings[] = ExternalSdCardOperation.getExtSdCardPathsForActivity(this); - for (String s : strings) { - File f = new File(s); - if (!rv.contains(s) && FileUtils.canListFiles(f)) rv.add(s); - } - } - File usb = getUsbDrive(); - if (usb != null && !rv.contains(usb.getPath())) rv.add(usb.getPath()); + if (SDK_INT >= M && checkStoragePermission()) rv.clear(); + if (SDK_INT >= KITKAT) { + String strings[] = ExternalSdCardOperation.getExtSdCardPathsForActivity(this); + for (String s : strings) { + File f = new File(s); + if (!rv.contains(s) && FileUtils.canListFiles(f)) rv.add(s); + } + } + File usb = getUsbDrive(); + if (usb != null && !rv.contains(usb.getPath())) rv.add(usb.getPath()); - if (SDK_INT >= KITKAT) { - if (SingletonUsbOtg.getInstance().isDeviceConnected()) { - rv.add(OTGUtil.PREFIX_OTG + "/"); - } - } + if (SDK_INT >= KITKAT) { + if (SingletonUsbOtg.getInstance().isDeviceConnected()) { + rv.add(OTGUtil.PREFIX_OTG + "/"); + } + } - // Assign a label and icon to each directory - ArrayList volumes = new ArrayList<>(); - for (String file : rv) { - File f = new File(file); - @DrawableRes int icon; - - if ("/storage/emulated/legacy".equals(file) - || "/storage/emulated/0".equals(file) - || "/mnt/sdcard".equals(file)) { - icon = R.drawable.ic_phone_android_white_24dp; - } else if ("/storage/sdcard1".equals(file)) { - icon = R.drawable.ic_sd_storage_white_24dp; - } else if ("/".equals(file)) { - icon = R.drawable.ic_drawer_root_white; - } else { - icon = R.drawable.ic_sd_storage_white_24dp; - } + // Assign a label and icon to each directory + ArrayList volumes = new ArrayList<>(); + for (String file : rv) { + File f = new File(file); + @DrawableRes int icon; + + if ("/storage/emulated/legacy".equals(file) + || "/storage/emulated/0".equals(file) + || "/mnt/sdcard".equals(file)) { + icon = R.drawable.ic_phone_android_white_24dp; + } else if ("/storage/sdcard1".equals(file)) { + icon = R.drawable.ic_sd_storage_white_24dp; + } else if ("/".equals(file)) { + icon = R.drawable.ic_drawer_root_white; + } else { + icon = R.drawable.ic_sd_storage_white_24dp; + } - @StorageNaming.DeviceDescription - int deviceDescription = StorageNaming.getDeviceDescriptionLegacy(f); - String name = StorageNamingHelper.getNameForDeviceDescription(this, f, deviceDescription); + @StorageNaming.DeviceDescription + int deviceDescription = StorageNaming.getDeviceDescriptionLegacy(f); + String name = StorageNamingHelper.getNameForDeviceDescription(this, f, deviceDescription); - volumes.add(new StorageDirectoryParcelable(file, name, icon)); + volumes.add(new StorageDirectoryParcelable(file, name, icon)); + } + + return volumes; } - return volumes; - } + @Override + public void onBackPressed() { + if (!drawer.isLocked() && drawer.isOpen()) { + drawer.close(); + return; + } - @Override - public void onBackPressed() { - if (!drawer.isLocked() && drawer.isOpen()) { - drawer.close(); - return; + Fragment fragment = getFragmentAtFrame(); + if (getAppbar().getSearchView().isShown()) { + // hide search view if visible, with an animation + getAppbar().getSearchView().hideSearchView(); + } else if (fragment instanceof TabFragment) { + if (floatingActionButton.isOpen()) { + floatingActionButton.close(true); + } else { + executeWithMainFragment( + mainFragment -> { + mainFragment.goBack(); + return null; + }); + } + } else if (fragment instanceof CompressedExplorerFragment) { + CompressedExplorerFragment compressedExplorerFragment = + (CompressedExplorerFragment) getFragmentAtFrame(); + if (compressedExplorerFragment.mActionMode == null) { + if (compressedExplorerFragment.canGoBack()) { + compressedExplorerFragment.goBack(); + } else if (isCompressedOpen) { + isCompressedOpen = false; + finish(); + } else { + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.setCustomAnimations(R.anim.slide_out_bottom, R.anim.slide_out_bottom); + fragmentTransaction.remove(compressedExplorerFragment); + fragmentTransaction.commit(); + supportInvalidateOptionsMenu(); + floatingActionButton.show(); + } + } else { + compressedExplorerFragment.mActionMode.finish(); + } + } else if (fragment instanceof FtpServerFragment) { + // returning back from FTP server + if (path != null && path.length() > 0) { + HybridFile file = new HybridFile(OpenMode.UNKNOWN, path); + file.generateMode(this); + if (file.isDirectory(this)) goToMain(path); + else { + goToMain(null); + FileUtils.openFile(new File(path), this, getPrefs()); + } + } else { + goToMain(null); + } + } else { + goToMain(null); + } } - Fragment fragment = getFragmentAtFrame(); - if (getAppbar().getSearchView().isShown()) { - // hide search view if visible, with an animation - getAppbar().getSearchView().hideSearchView(); - } else if (fragment instanceof TabFragment) { - if (floatingActionButton.isOpen()) { - floatingActionButton.close(true); - } else { - executeWithMainFragment( - mainFragment -> { - mainFragment.goBack(); - return null; - }); - } - } else if (fragment instanceof CompressedExplorerFragment) { - CompressedExplorerFragment compressedExplorerFragment = - (CompressedExplorerFragment) getFragmentAtFrame(); - if (compressedExplorerFragment.mActionMode == null) { - if (compressedExplorerFragment.canGoBack()) { - compressedExplorerFragment.goBack(); - } else if (isCompressedOpen) { - isCompressedOpen = false; - finish(); - } else { - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - fragmentTransaction.setCustomAnimations(R.anim.slide_out_bottom, R.anim.slide_out_bottom); - fragmentTransaction.remove(compressedExplorerFragment); - fragmentTransaction.commit(); - supportInvalidateOptionsMenu(); - floatingActionButton.show(); - } - } else { - compressedExplorerFragment.mActionMode.finish(); - } - } else if (fragment instanceof FtpServerFragment) { - // returning back from FTP server - if (path != null && path.length() > 0) { - HybridFile file = new HybridFile(OpenMode.UNKNOWN, path); - file.generateMode(this); - if (file.isDirectory(this)) goToMain(path); - else { - goToMain(null); - FileUtils.openFile(new File(path), this, getPrefs()); - } - } else { - goToMain(null); - } - } else { - goToMain(null); + public void invalidatePasteSnackbar(boolean showSnackbar) { + if (pasteHelper != null) { + pasteHelper.invalidateSnackbar(this, showSnackbar); + } } - } - public void invalidatePasteSnackbar(boolean showSnackbar) { - if (pasteHelper != null) { - pasteHelper.invalidateSnackbar(this, showSnackbar); + public void exit() { + if (backPressedToExitOnce) { + NetCopyClientConnectionPool.INSTANCE.shutdown(); + finish(); + if (isRootExplorer()) { + closeInteractiveShell(); + } + } else { + this.backPressedToExitOnce = true; + final Toast toast = Toast.makeText(this, getString(R.string.press_again), Toast.LENGTH_SHORT); + this.toast = new WeakReference<>(toast); + toast.show(); + new Handler() + .postDelayed( + () -> { + backPressedToExitOnce = false; + }, + 2000); + } } - } - public void exit() { - if (backPressedToExitOnce) { - NetCopyClientConnectionPool.INSTANCE.shutdown(); - finish(); - if (isRootExplorer()) { - closeInteractiveShell(); - } - } else { - this.backPressedToExitOnce = true; - final Toast toast = Toast.makeText(this, getString(R.string.press_again), Toast.LENGTH_SHORT); - this.toast = new WeakReference<>(toast); - toast.show(); - new Handler() - .postDelayed( - () -> { - backPressedToExitOnce = false; - }, - 2000); - } - } - - public void goToMain(String path) { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - // title.setText(R.string.app_name); - TabFragment tabFragment = new TabFragment(); - if (intent != null && intent.getAction() != null) { - if (INTENT_ACTION_OPEN_QUICK_ACCESS.equals(intent.getAction())) { - path = "5"; - } else if (INTENT_ACTION_OPEN_RECENT.equals(intent.getAction())) { - path = "6"; - } + public void goToMain(String path) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + // title.setText(R.string.app_name); + TabFragment tabFragment = new TabFragment(); + if (intent != null && intent.getAction() != null) { + if (INTENT_ACTION_OPEN_QUICK_ACCESS.equals(intent.getAction())) { + path = "5"; + } else if (INTENT_ACTION_OPEN_RECENT.equals(intent.getAction())) { + path = "6"; + } + } + if (path != null && path.length() > 0) { + Bundle b = new Bundle(); + b.putString("path", path); + tabFragment.setArguments(b); + } + transaction.replace(R.id.content_frame, tabFragment); + // Commit the transaction + transaction.addToBackStack("tabt" + 1); + transaction.commitAllowingStateLoss(); + appbar.setTitle(null); + floatingActionButton.show(); + if (isCompressedOpen && pathInCompressedArchive != null) { + openCompressed(pathInCompressedArchive); + pathInCompressedArchive = null; + } } - if (path != null && path.length() > 0) { - Bundle b = new Bundle(); - b.putString("path", path); - tabFragment.setArguments(b); - } - transaction.replace(R.id.content_frame, tabFragment); - // Commit the transaction - transaction.addToBackStack("tabt" + 1); - transaction.commitAllowingStateLoss(); - appbar.setTitle(null); - floatingActionButton.show(); - if (isCompressedOpen && pathInCompressedArchive != null) { - openCompressed(pathInCompressedArchive); - pathInCompressedArchive = null; - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater menuInflater = getMenuInflater(); - menuInflater.inflate(R.menu.activity_extra, menu); + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.activity_extra, menu); /* SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); SearchView searchView = (SearchView) menu.findItem(R.id.search).getActionView(); @@ -1018,236 +1044,236 @@ public boolean onMenuItemActionCollapse(MenuItem item) { } }); */ - return super.onCreateOptionsMenu(menu); - } + return super.onCreateOptionsMenu(menu); + } - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem s = menu.findItem(R.id.view); - MenuItem search = menu.findItem(R.id.search); - Fragment fragment = getFragmentAtFrame(); - if (fragment instanceof TabFragment) { - appbar.setTitle(R.string.appbar_name); - if (getBoolean(PREFERENCE_VIEW)) { - s.setTitle(getResources().getString(R.string.gridview)); - } else { - s.setTitle(getResources().getString(R.string.listview)); - } - try { - executeWithMainFragment( - mainFragment -> { - if (mainFragment.getMainFragmentViewModel().isList()) { - s.setTitle(R.string.gridview); - } else { - s.setTitle(R.string.listview); - } - appbar - .getBottomBar() - .updatePath( - mainFragment.getCurrentPath(), - mainFragment.getMainFragmentViewModel().getResults(), - MainActivityHelper.SEARCH_TEXT, - mainFragment.getMainFragmentViewModel().getOpenMode(), - mainFragment.getMainFragmentViewModel().getFolderCount(), - mainFragment.getMainFragmentViewModel().getFileCount(), - mainFragment); - return null; - }); - } catch (Exception e) { - LOG.warn("failure while preparing options menu", e); - } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem s = menu.findItem(R.id.view); + MenuItem search = menu.findItem(R.id.search); + Fragment fragment = getFragmentAtFrame(); + if (fragment instanceof TabFragment) { + appbar.setTitle(R.string.appbar_name); + if (getBoolean(PREFERENCE_VIEW)) { + s.setTitle(getResources().getString(R.string.gridview)); + } else { + s.setTitle(getResources().getString(R.string.listview)); + } + try { + executeWithMainFragment( + mainFragment -> { + if (mainFragment.getMainFragmentViewModel().isList()) { + s.setTitle(R.string.gridview); + } else { + s.setTitle(R.string.listview); + } + appbar + .getBottomBar() + .updatePath( + mainFragment.getCurrentPath(), + mainFragment.getMainFragmentViewModel().getResults(), + MainActivityHelper.SEARCH_TEXT, + mainFragment.getMainFragmentViewModel().getOpenMode(), + mainFragment.getMainFragmentViewModel().getFolderCount(), + mainFragment.getMainFragmentViewModel().getFileCount(), + mainFragment); + return null; + }); + } catch (Exception e) { + LOG.warn("failure while preparing options menu", e); + } - appbar.getBottomBar().setClickListener(); - - search.setVisible(true); - if (indicator_layout != null) indicator_layout.setVisibility(View.VISIBLE); - menu.findItem(R.id.search).setVisible(true); - menu.findItem(R.id.home).setVisible(true); - menu.findItem(R.id.history).setVisible(true); - menu.findItem(R.id.sethome).setVisible(true); - menu.findItem(R.id.sort).setVisible(true); - menu.findItem(R.id.hiddenitems).setVisible(true); - menu.findItem(R.id.view).setVisible(true); - menu.findItem(R.id.extract).setVisible(false); - invalidatePasteSnackbar(true); - findViewById(R.id.buttonbarframe).setVisibility(View.VISIBLE); - } else if (fragment instanceof AppsListFragment - || fragment instanceof ProcessViewerFragment - || fragment instanceof FtpServerFragment) { - appBarLayout.setExpanded(true); - menu.findItem(R.id.sethome).setVisible(false); - if (indicator_layout != null) indicator_layout.setVisibility(View.GONE); - findViewById(R.id.buttonbarframe).setVisibility(View.GONE); - menu.findItem(R.id.search).setVisible(false); - menu.findItem(R.id.home).setVisible(false); - menu.findItem(R.id.history).setVisible(false); - menu.findItem(R.id.extract).setVisible(false); - if (fragment instanceof ProcessViewerFragment) { - menu.findItem(R.id.sort).setVisible(false); - } else if (fragment instanceof FtpServerFragment) { - menu.findItem(R.id.sort).setVisible(false); - } else { - menu.findItem(R.id.dsort).setVisible(false); - menu.findItem(R.id.sortby).setVisible(false); - } - menu.findItem(R.id.hiddenitems).setVisible(false); - menu.findItem(R.id.view).setVisible(false); - invalidatePasteSnackbar(false); - } else if (fragment instanceof CompressedExplorerFragment) { - appbar.setTitle(R.string.appbar_name); - menu.findItem(R.id.sethome).setVisible(false); - if (indicator_layout != null) indicator_layout.setVisibility(View.GONE); - getAppbar().getBottomBar().resetClickListener(); - menu.findItem(R.id.search).setVisible(false); - menu.findItem(R.id.home).setVisible(false); - menu.findItem(R.id.history).setVisible(false); - menu.findItem(R.id.sort).setVisible(false); - menu.findItem(R.id.hiddenitems).setVisible(false); - menu.findItem(R.id.view).setVisible(false); - menu.findItem(R.id.extract).setVisible(true); - invalidatePasteSnackbar(false); - } - return super.onPrepareOptionsMenu(menu); - } - - // called when the user exits the action mode - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // The action bar home/up action should open or close the drawer. - // ActionBarDrawerToggle will take care of this. - if (drawer.onOptionsItemSelected(item)) return true; - // Same thing goes to other Fragments loaded. - // If they have handled the options, we don't need to. - if (getFragmentAtFrame().onOptionsItemSelected(item)) return true; - - // Handle action buttons - executeWithMainFragment( - mainFragment -> { - switch (item.getItemId()) { - case R.id.home: - mainFragment.home(); - break; - case R.id.history: - HistoryDialog.showHistoryDialog(this, mainFragment); - break; - case R.id.sethome: - if (mainFragment.getMainFragmentViewModel().getOpenMode() != OpenMode.FILE - && mainFragment.getMainFragmentViewModel().getOpenMode() != OpenMode.ROOT) { - Toast.makeText(mainActivity, R.string.not_allowed, Toast.LENGTH_SHORT).show(); - break; - } - final MaterialDialog dialog = - GeneralDialogCreation.showBasicDialog( - mainActivity, - R.string.question_set_path_as_home, - R.string.set_as_home, - R.string.yes, - R.string.no); - dialog - .getActionButton(DialogAction.POSITIVE) - .setOnClickListener( - (v) -> { - mainFragment - .getMainFragmentViewModel() - .setHome(mainFragment.getCurrentPath()); - updatePaths(mainFragment.getMainFragmentViewModel().getNo()); - dialog.dismiss(); - }); - dialog.show(); - break; - case R.id.exit: - finish(); - break; - case R.id.sortby: - GeneralDialogCreation.showSortDialog(mainFragment, getAppTheme(), getPrefs()); - break; - case R.id.dsort: - String[] sort = getResources().getStringArray(R.array.directorysortmode); - MaterialDialog.Builder builder = new MaterialDialog.Builder(mainActivity); - builder.theme(getAppTheme().getMaterialDialogTheme(this)); - builder.title(R.string.directorysort); - int current = - Integer.parseInt( - getPrefs() - .getString(PreferencesConstants.PREFERENCE_DIRECTORY_SORT_MODE, "0")); - - builder - .items(sort) - .itemsCallbackSingleChoice( - current, - (dialog1, view, which, text) -> { - getPrefs() - .edit() - .putString( - PreferencesConstants.PREFERENCE_DIRECTORY_SORT_MODE, "" + which) - .commit(); - mainFragment - .getMainFragmentViewModel() - .initSortModes( - SortHandler.getSortType( - this, mainFragment.getMainFragmentViewModel().getCurrentPath()), - getPrefs()); - mainFragment.updateList(false); - dialog1.dismiss(); - return true; - }); - builder.build().show(); - break; - case R.id.hiddenitems: - HiddenFilesDialog.showHiddenDialog(this, mainFragment); - break; - case R.id.view: - int pathLayout = - dataUtils.getListOrGridForPath(mainFragment.getCurrentPath(), DataUtils.LIST); - if (mainFragment.getMainFragmentViewModel().isList()) { - if (pathLayout == DataUtils.LIST) { - AppConfig.getInstance() - .runInBackground( - () -> { - utilsHandler.removeFromDatabase( - new OperationData( - UtilsHandler.Operation.LIST, mainFragment.getCurrentPath())); - }); - } - utilsHandler.saveToDatabase( - new OperationData(UtilsHandler.Operation.GRID, mainFragment.getCurrentPath())); - - dataUtils.setPathAsGridOrList(mainFragment.getCurrentPath(), DataUtils.GRID); - } else { - if (pathLayout == DataUtils.GRID) { - AppConfig.getInstance() - .runInBackground( - () -> { - utilsHandler.removeFromDatabase( - new OperationData( - UtilsHandler.Operation.GRID, mainFragment.getCurrentPath())); - }); - } + appbar.getBottomBar().setClickListener(); + + search.setVisible(true); + if (indicator_layout != null) indicator_layout.setVisibility(View.VISIBLE); + menu.findItem(R.id.search).setVisible(true); + menu.findItem(R.id.home).setVisible(true); + menu.findItem(R.id.history).setVisible(true); + menu.findItem(R.id.sethome).setVisible(true); + menu.findItem(R.id.sort).setVisible(true); + menu.findItem(R.id.hiddenitems).setVisible(true); + menu.findItem(R.id.view).setVisible(true); + menu.findItem(R.id.extract).setVisible(false); + invalidatePasteSnackbar(true); + findViewById(R.id.buttonbarframe).setVisibility(View.VISIBLE); + } else if (fragment instanceof AppsListFragment + || fragment instanceof ProcessViewerFragment + || fragment instanceof FtpServerFragment) { + appBarLayout.setExpanded(true); + menu.findItem(R.id.sethome).setVisible(false); + if (indicator_layout != null) indicator_layout.setVisibility(View.GONE); + findViewById(R.id.buttonbarframe).setVisibility(View.GONE); + menu.findItem(R.id.search).setVisible(false); + menu.findItem(R.id.home).setVisible(false); + menu.findItem(R.id.history).setVisible(false); + menu.findItem(R.id.extract).setVisible(false); + if (fragment instanceof ProcessViewerFragment) { + menu.findItem(R.id.sort).setVisible(false); + } else if (fragment instanceof FtpServerFragment) { + menu.findItem(R.id.sort).setVisible(false); + } else { + menu.findItem(R.id.dsort).setVisible(false); + menu.findItem(R.id.sortby).setVisible(false); + } + menu.findItem(R.id.hiddenitems).setVisible(false); + menu.findItem(R.id.view).setVisible(false); + invalidatePasteSnackbar(false); + } else if (fragment instanceof CompressedExplorerFragment) { + appbar.setTitle(R.string.appbar_name); + menu.findItem(R.id.sethome).setVisible(false); + if (indicator_layout != null) indicator_layout.setVisibility(View.GONE); + getAppbar().getBottomBar().resetClickListener(); + menu.findItem(R.id.search).setVisible(false); + menu.findItem(R.id.home).setVisible(false); + menu.findItem(R.id.history).setVisible(false); + menu.findItem(R.id.sort).setVisible(false); + menu.findItem(R.id.hiddenitems).setVisible(false); + menu.findItem(R.id.view).setVisible(false); + menu.findItem(R.id.extract).setVisible(true); + invalidatePasteSnackbar(false); + } + return super.onPrepareOptionsMenu(menu); + } - utilsHandler.saveToDatabase( - new OperationData(UtilsHandler.Operation.LIST, mainFragment.getCurrentPath())); - - dataUtils.setPathAsGridOrList(mainFragment.getCurrentPath(), DataUtils.LIST); - } - mainFragment.switchView(); - break; - case R.id.extract: - Fragment fragment1 = getFragmentAtFrame(); - if (fragment1 instanceof CompressedExplorerFragment) { - mainActivityHelper.extractFile( - ((CompressedExplorerFragment) fragment1).compressedFile); - } - break; - case R.id.search: - getAppbar().getSearchView().revealSearchView(); - break; - } - return null; - }, - false); - - return super.onOptionsItemSelected(item); - } + // called when the user exits the action mode + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // The action bar home/up action should open or close the drawer. + // ActionBarDrawerToggle will take care of this. + if (drawer.onOptionsItemSelected(item)) return true; + // Same thing goes to other Fragments loaded. + // If they have handled the options, we don't need to. + if (getFragmentAtFrame().onOptionsItemSelected(item)) return true; + + // Handle action buttons + executeWithMainFragment( + mainFragment -> { + switch (item.getItemId()) { + case R.id.home: + mainFragment.home(); + break; + case R.id.history: + HistoryDialog.showHistoryDialog(this, mainFragment); + break; + case R.id.sethome: + if (mainFragment.getMainFragmentViewModel().getOpenMode() != OpenMode.FILE + && mainFragment.getMainFragmentViewModel().getOpenMode() != OpenMode.ROOT) { + Toast.makeText(mainActivity, R.string.not_allowed, Toast.LENGTH_SHORT).show(); + break; + } + final MaterialDialog dialog = + GeneralDialogCreation.showBasicDialog( + mainActivity, + R.string.question_set_path_as_home, + R.string.set_as_home, + R.string.yes, + R.string.no); + dialog + .getActionButton(DialogAction.POSITIVE) + .setOnClickListener( + (v) -> { + mainFragment + .getMainFragmentViewModel() + .setHome(mainFragment.getCurrentPath()); + updatePaths(mainFragment.getMainFragmentViewModel().getNo()); + dialog.dismiss(); + }); + dialog.show(); + break; + case R.id.exit: + finish(); + break; + case R.id.sortby: + GeneralDialogCreation.showSortDialog(mainFragment, getAppTheme(), getPrefs()); + break; + case R.id.dsort: + String[] sort = getResources().getStringArray(R.array.directorysortmode); + MaterialDialog.Builder builder = new MaterialDialog.Builder(mainActivity); + builder.theme(getAppTheme().getMaterialDialogTheme(this)); + builder.title(R.string.directorysort); + int current = + Integer.parseInt( + getPrefs() + .getString(PreferencesConstants.PREFERENCE_DIRECTORY_SORT_MODE, "0")); + + builder + .items(sort) + .itemsCallbackSingleChoice( + current, + (dialog1, view, which, text) -> { + getPrefs() + .edit() + .putString( + PreferencesConstants.PREFERENCE_DIRECTORY_SORT_MODE, "" + which) + .commit(); + mainFragment + .getMainFragmentViewModel() + .initSortModes( + SortHandler.getSortType( + this, mainFragment.getMainFragmentViewModel().getCurrentPath()), + getPrefs()); + mainFragment.updateList(false); + dialog1.dismiss(); + return true; + }); + builder.build().show(); + break; + case R.id.hiddenitems: + HiddenFilesDialog.showHiddenDialog(this, mainFragment); + break; + case R.id.view: + int pathLayout = + dataUtils.getListOrGridForPath(mainFragment.getCurrentPath(), DataUtils.LIST); + if (mainFragment.getMainFragmentViewModel().isList()) { + if (pathLayout == DataUtils.LIST) { + AppConfig.getInstance() + .runInBackground( + () -> { + utilsHandler.removeFromDatabase( + new OperationData( + UtilsHandler.Operation.LIST, mainFragment.getCurrentPath())); + }); + } + utilsHandler.saveToDatabase( + new OperationData(UtilsHandler.Operation.GRID, mainFragment.getCurrentPath())); + + dataUtils.setPathAsGridOrList(mainFragment.getCurrentPath(), DataUtils.GRID); + } else { + if (pathLayout == DataUtils.GRID) { + AppConfig.getInstance() + .runInBackground( + () -> { + utilsHandler.removeFromDatabase( + new OperationData( + UtilsHandler.Operation.GRID, mainFragment.getCurrentPath())); + }); + } + + utilsHandler.saveToDatabase( + new OperationData(UtilsHandler.Operation.LIST, mainFragment.getCurrentPath())); + + dataUtils.setPathAsGridOrList(mainFragment.getCurrentPath(), DataUtils.LIST); + } + mainFragment.switchView(); + break; + case R.id.extract: + Fragment fragment1 = getFragmentAtFrame(); + if (fragment1 instanceof CompressedExplorerFragment) { + mainActivityHelper.extractFile( + ((CompressedExplorerFragment) fragment1).compressedFile); + } + break; + case R.id.search: + getAppbar().getSearchView().revealSearchView(); + break; + } + return null; + }, + false); + + return super.onOptionsItemSelected(item); + } /*@Override public void onRestoreInstanceState(Bundle savedInstanceState){ @@ -1261,1254 +1287,1265 @@ public void onRestoreInstanceState(Bundle savedInstanceState){ selectedStorage = savedInstanceState.getInt(KEY_DRAWER_SELECTED, 0); }*/ - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - // Sync the toggle state after onRestoreInstanceState has occurred. - drawer.syncState(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - // Pass any configuration change to the drawer toggls - drawer.onConfigurationChanged(newConfig); - } - - @Override - 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); - } - - if (oppathe != null) { - outState.putString(KEY_OPERATION_PATH, oppathe); - outState.putString(KEY_OPERATED_ON_PATH, oppathe1); - outState.putParcelableArrayList(KEY_OPERATIONS_PATH_LIST, (oparrayList)); - outState.putInt(KEY_OPERATION, operation); - } - } - - @Override - protected void onPause() { - super.onPause(); - unregisterReceiver(mainActivityHelper.mNotificationReceiver); - unregisterReceiver(receiver2); - - if (SDK_INT >= KITKAT) { - unregisterReceiver(mOtgReceiver); - } - - final Toast toast = this.toast.get(); - if (toast != null) { - toast.cancel(); - } - this.toast = new WeakReference<>(null); - } - - @Override - public void onResume() { - super.onResume(); - if (materialDialog != null && !materialDialog.isShowing()) { - materialDialog.show(); - materialDialog = null; - } - - drawer.refreshDrawer(); - drawer.refactorDrawerLockMode(); - - IntentFilter newFilter = new IntentFilter(); - newFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); - newFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); - newFilter.addDataScheme(ContentResolver.SCHEME_FILE); - registerReceiver(mainActivityHelper.mNotificationReceiver, newFilter); - registerReceiver(receiver2, new IntentFilter(TAG_INTENT_FILTER_GENERAL)); - - if (SDK_INT >= Build.VERSION_CODES.KITKAT) { - updateUsbInformation(); - } - } - - /** Updates everything related to USB devices MUST ALWAYS be called after onResume() */ - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - private void updateUsbInformation() { - boolean isInformationUpdated = false; - List connectedDevices = OTGUtil.getMassStorageDevicesConnected(this); - - if (!connectedDevices.isEmpty()) { - if (SingletonUsbOtg.getInstance().getUsbOtgRoot() != null - && OTGUtil.isUsbUriAccessible(this)) { - for (UsbOtgRepresentation device : connectedDevices) { - if (SingletonUsbOtg.getInstance().checkIfRootIsFromDevice(device)) { - isInformationUpdated = true; - break; - } + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + // Sync the toggle state after onRestoreInstanceState has occurred. + drawer.syncState(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + drawer.onConfigurationChanged(newConfig); + // Pass any configuration change to the drawer toggls + } + + @Override + 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); } - if (!isInformationUpdated) { - SingletonUsbOtg.getInstance().resetUsbOtgRoot(); + if (oppathe != null) { + outState.putString(KEY_OPERATION_PATH, oppathe); + outState.putString(KEY_OPERATED_ON_PATH, oppathe1); + outState.putParcelableArrayList(KEY_OPERATIONS_PATH_LIST, (oparrayList)); + outState.putInt(KEY_OPERATION, operation); } - } + } - if (!isInformationUpdated) { - SingletonUsbOtg.getInstance().setConnectedDevice(connectedDevices.get(0)); - isInformationUpdated = true; - } + @Override + protected void onPause() { + super.onPause(); + unregisterReceiver(mainActivityHelper.mNotificationReceiver); + unregisterReceiver(receiver2); + + if (SDK_INT >= KITKAT) { + unregisterReceiver(mOtgReceiver); + } + + final Toast toast = this.toast.get(); + if (toast != null) { + toast.cancel(); + } + this.toast = new WeakReference<>(null); } - if (!isInformationUpdated) { - SingletonUsbOtg.getInstance().resetUsbOtgRoot(); - drawer.refreshDrawer(); + @Override + public void onResume() { + super.onResume(); + if (materialDialog != null && !materialDialog.isShowing()) { + materialDialog.show(); + materialDialog = null; + } + + drawer.refreshDrawer(); + drawer.refactorDrawerLockMode(); + + IntentFilter newFilter = new IntentFilter(); + newFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); + newFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + newFilter.addDataScheme(ContentResolver.SCHEME_FILE); + registerReceiver(mainActivityHelper.mNotificationReceiver, newFilter); + registerReceiver(receiver2, new IntentFilter(TAG_INTENT_FILTER_GENERAL)); + + if (SDK_INT >= Build.VERSION_CODES.KITKAT) { + updateUsbInformation(); + } } - // Registering intent filter for OTG - IntentFilter otgFilter = new IntentFilter(); - otgFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); - otgFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); - registerReceiver(mOtgReceiver, otgFilter); - } + /** + * Updates everything related to USB devices MUST ALWAYS be called after onResume() + */ + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private void updateUsbInformation() { + boolean isInformationUpdated = false; + List connectedDevices = OTGUtil.getMassStorageDevicesConnected(this); + + if (!connectedDevices.isEmpty()) { + if (SingletonUsbOtg.getInstance().getUsbOtgRoot() != null + && OTGUtil.isUsbUriAccessible(this)) { + for (UsbOtgRepresentation device : connectedDevices) { + if (SingletonUsbOtg.getInstance().checkIfRootIsFromDevice(device)) { + isInformationUpdated = true; + break; + } + } - /** Receiver to check if a USB device is connected at the runtime of application */ - BroadcastReceiver mOtgReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { - List connectedDevices = - OTGUtil.getMassStorageDevicesConnected(MainActivity.this); - if (!connectedDevices.isEmpty()) { - SingletonUsbOtg.getInstance().resetUsbOtgRoot(); - SingletonUsbOtg.getInstance().setConnectedDevice(connectedDevices.get(0)); - drawer.refreshDrawer(); + if (!isInformationUpdated) { + SingletonUsbOtg.getInstance().resetUsbOtgRoot(); + } } - } else if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + + if (!isInformationUpdated) { + SingletonUsbOtg.getInstance().setConnectedDevice(connectedDevices.get(0)); + isInformationUpdated = true; + } + } + + if (!isInformationUpdated) { SingletonUsbOtg.getInstance().resetUsbOtgRoot(); drawer.refreshDrawer(); - goToMain(null); - } } - }; - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_MENU) { + // Registering intent filter for OTG + IntentFilter otgFilter = new IntentFilter(); + otgFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + otgFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + registerReceiver(mOtgReceiver, otgFilter); + } + + /** + * Receiver to check if a USB device is connected at the runtime of application + */ + BroadcastReceiver mOtgReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { + List connectedDevices = + OTGUtil.getMassStorageDevicesConnected(MainActivity.this); + if (!connectedDevices.isEmpty()) { + SingletonUsbOtg.getInstance().resetUsbOtgRoot(); + SingletonUsbOtg.getInstance().setConnectedDevice(connectedDevices.get(0)); + drawer.refreshDrawer(); + } + } else if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + SingletonUsbOtg.getInstance().resetUsbOtgRoot(); + drawer.refreshDrawer(); + goToMain(null); + } + } + }; + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU) { /* ImageView ib = findViewById(R.id.action_overflow); if (ib.getVisibility() == View.VISIBLE) { ib.performClick(); } */ - // return 'true' to prevent further propagation of the key event - return true; - } - - return super.onKeyDown(keyCode, event); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - // TODO: 6/5/2017 Android may choose to not call this method before destruction - // TODO: https://developer.android.com/reference/android/app/Activity.html#onDestroy%28%29 - closeInteractiveShell(); - NetCopyClientConnectionPool.INSTANCE.shutdown(); - if (drawer != null && drawer.getBilling() != null) { - drawer.getBilling().destroyBillingInstance(); - } - } - - /** Closes the interactive shell and threads associated */ - private void closeInteractiveShell() { - if (isRootExplorer()) { - // close interactive shell - try { - Shell.getShell().close(); - } catch (IOException e) { - LOG.error("Error closing Shell", e); - } + // return 'true' to prevent further propagation of the key event + return true; + } + + return super.onKeyDown(keyCode, event); } - } - - public void updatePaths(int pos) { - TabFragment tabFragment = getTabFragment(); - if (tabFragment != null) tabFragment.updatePaths(pos); - } - - public void openCompressed(String path) { - appBarLayout.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start(); - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - fragmentTransaction.setCustomAnimations(R.anim.slide_in_top, R.anim.slide_in_bottom); - Fragment zipFragment = new CompressedExplorerFragment(); - Bundle bundle = new Bundle(); - bundle.putString(CompressedExplorerFragment.KEY_PATH, path); - zipFragment.setArguments(bundle); - fragmentTransaction.add(R.id.content_frame, zipFragment); - fragmentTransaction.commitAllowingStateLoss(); - } - - public @Nullable MainFragment getCurrentMainFragment() { - TabFragment tab = getTabFragment(); - - if (tab != null && tab.getCurrentTabFragment() instanceof MainFragment) { - return (MainFragment) tab.getCurrentTabFragment(); - } else return null; - } - - public TabFragment getTabFragment() { - Fragment fragment = getFragmentAtFrame(); - - if (!(fragment instanceof TabFragment)) return null; - else return (TabFragment) fragment; - } - - public Fragment getFragmentAtFrame() { - return getSupportFragmentManager().findFragmentById(R.id.content_frame); - } - - public void setPagingEnabled(boolean b) { - getTabFragment().setPagingEnabled(b); - } - - public File getUsbDrive() { - File parent = new File("/storage"); - - try { - for (File f : parent.listFiles()) - if (f.exists() && f.getName().toLowerCase().contains("usb") && f.canExecute()) return f; - } catch (Exception e) { - } - - parent = new File("/mnt/sdcard/usbStorage"); - if (parent.exists() && parent.canExecute()) return parent; - parent = new File("/mnt/sdcard/usb_storage"); - if (parent.exists() && parent.canExecute()) return parent; - - return null; - } - - public SpeedDialView getFAB() { - return floatingActionButton; - } - - public void showFab() { - getFAB().setVisibility(View.VISIBLE); - getFAB().show(); - CoordinatorLayout.LayoutParams params = - (CoordinatorLayout.LayoutParams) getFAB().getLayoutParams(); - params.setBehavior(new SpeedDialView.ScrollingViewSnackbarBehavior()); - getFAB().requestLayout(); - } - - public void hideFab() { - getFAB().setVisibility(View.GONE); - getFAB().hide(); - CoordinatorLayout.LayoutParams params = - (CoordinatorLayout.LayoutParams) getFAB().getLayoutParams(); - params.setBehavior(new SpeedDialView.NoBehavior()); - getFAB().requestLayout(); - } - - public AppBar getAppbar() { - return appbar; - } - - public Drawer getDrawer() { - return drawer; - } - - protected void onActivityResult(int requestCode, int responseCode, Intent intent) { - super.onActivityResult(requestCode, responseCode, intent); - if (requestCode == Drawer.image_selector_request_code) { - drawer.onActivityResult(requestCode, responseCode, intent); - } else if (requestCode == 3) { - Uri treeUri; - if (responseCode == Activity.RESULT_OK) { - // Get Uri from Storage Access Framework. - treeUri = intent.getData(); - // Persist URI - this is required for verification of writability. - if (treeUri != null) - getPrefs() - .edit() - .putString(PreferencesConstants.PREFERENCE_URI, treeUri.toString()) - .apply(); - } else { - // If not confirmed SAF, or if still not writable, then revert settings. + + @Override + protected void onDestroy() { + super.onDestroy(); + // TODO: 6/5/2017 Android may choose to not call this method before destruction + // TODO: https://developer.android.com/reference/android/app/Activity.html#onDestroy%28%29 + closeInteractiveShell(); + NetCopyClientConnectionPool.INSTANCE.shutdown(); + if (drawer != null && drawer.getBilling() != null) { + drawer.getBilling().destroyBillingInstance(); + } + } + + /** + * Closes the interactive shell and threads associated + */ + private void closeInteractiveShell() { + if (isRootExplorer()) { + // close interactive shell + try { + Shell.getShell().close(); + } catch (IOException e) { + LOG.error("Error closing Shell", e); + } + } + } + + public void updatePaths(int pos) { + TabFragment tabFragment = getTabFragment(); + if (tabFragment != null) tabFragment.updatePaths(pos); + } + + public void openCompressed(String path) { + appBarLayout.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start(); + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.setCustomAnimations(R.anim.slide_in_top, R.anim.slide_in_bottom); + Fragment zipFragment = new CompressedExplorerFragment(); + Bundle bundle = new Bundle(); + bundle.putString(CompressedExplorerFragment.KEY_PATH, path); + zipFragment.setArguments(bundle); + fragmentTransaction.add(R.id.content_frame, zipFragment); + fragmentTransaction.commitAllowingStateLoss(); + } + + public @Nullable + MainFragment getCurrentMainFragment() { + TabFragment tab = getTabFragment(); + + if (tab != null && tab.getCurrentTabFragment() instanceof MainFragment) { + return (MainFragment) tab.getCurrentTabFragment(); + } else return null; + } + + public TabFragment getTabFragment() { + Fragment fragment = getFragmentAtFrame(); + + if (!(fragment instanceof TabFragment)) return null; + else return (TabFragment) fragment; + } + + public Fragment getFragmentAtFrame() { + return getSupportFragmentManager().findFragmentById(R.id.content_frame); + } + + public void setPagingEnabled(boolean b) { + getTabFragment().setPagingEnabled(b); + } + + public File getUsbDrive() { + File parent = new File("/storage"); + + try { + for (File f : parent.listFiles()) + if (f.exists() && f.getName().toLowerCase().contains("usb") && f.canExecute()) + return f; + } catch (Exception e) { + } + + parent = new File("/mnt/sdcard/usbStorage"); + if (parent.exists() && parent.canExecute()) return parent; + parent = new File("/mnt/sdcard/usb_storage"); + if (parent.exists() && parent.canExecute()) return parent; + + return null; + } + + public SpeedDialView getFAB() { + return floatingActionButton; + } + + public void showFab() { + getFAB().setVisibility(View.VISIBLE); + getFAB().show(); + CoordinatorLayout.LayoutParams params = + (CoordinatorLayout.LayoutParams) getFAB().getLayoutParams(); + params.setBehavior(new SpeedDialView.ScrollingViewSnackbarBehavior()); + getFAB().requestLayout(); + } + + public void hideFab() { + getFAB().setVisibility(View.GONE); + getFAB().hide(); + CoordinatorLayout.LayoutParams params = + (CoordinatorLayout.LayoutParams) getFAB().getLayoutParams(); + params.setBehavior(new SpeedDialView.NoBehavior()); + getFAB().requestLayout(); + } + + public AppBar getAppbar() { + return appbar; + } + + public Drawer getDrawer() { + return drawer; + } + + protected void onActivityResult(int requestCode, int responseCode, Intent intent) { + super.onActivityResult(requestCode, responseCode, intent); + if (requestCode == Drawer.image_selector_request_code) { + drawer.onActivityResult(requestCode, responseCode, intent); + } else if (requestCode == 3) { + Uri treeUri; + if (responseCode == Activity.RESULT_OK) { + // Get Uri from Storage Access Framework. + treeUri = intent.getData(); + // Persist URI - this is required for verification of writability. + if (treeUri != null) + getPrefs() + .edit() + .putString(PreferencesConstants.PREFERENCE_URI, treeUri.toString()) + .apply(); + } else { + // If not confirmed SAF, or if still not writable, then revert settings. /* DialogUtil.displayError(getActivity(), R.string.message_dialog_cannot_write_to_folder_saf, false, currentFolder); ||!FileUtil.isWritableNormalOrSaf(currentFolder)*/ - return; - } + return; + } - // After confirmation, update stored value of folder. - // Persist access permissions. + // After confirmation, update stored value of folder. + // Persist access permissions. - if (SDK_INT >= KITKAT) { - getContentResolver() - .takePersistableUriPermission( - treeUri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } + if (SDK_INT >= KITKAT) { + getContentResolver() + .takePersistableUriPermission( + treeUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } - executeWithMainFragment( - mainFragment -> { - switch (operation) { - case DELETE: // deletion - new DeleteTask(mainActivity).execute((oparrayList)); - break; - case COPY: // copying - // legacy compatibility - if (oparrayList != null && oparrayList.size() != 0) { - oparrayListList = new ArrayList<>(); - oparrayListList.add(oparrayList); - oparrayList = null; - oppatheList = new ArrayList<>(); - oppatheList.add(oppathe); - oppathe = ""; - } - for (int i = 0; i < oparrayListList.size(); i++) { - ArrayList sourceList = oparrayListList.get(i); - Intent intent1 = new Intent(this, CopyService.class); - intent1.putExtra(CopyService.TAG_COPY_SOURCES, sourceList); - intent1.putExtra(CopyService.TAG_COPY_TARGET, oppatheList.get(i)); - ServiceWatcherUtil.runService(this, intent1); - } - break; - case MOVE: // moving - // legacy compatibility - if (oparrayList != null && oparrayList.size() != 0) { - oparrayListList = new ArrayList<>(); - oparrayListList.add(oparrayList); - oparrayList = null; - oppatheList = new ArrayList<>(); - oppatheList.add(oppathe); - oppathe = ""; - } + executeWithMainFragment( + mainFragment -> { + switch (operation) { + case DELETE: // deletion + new DeleteTask(mainActivity).execute((oparrayList)); + break; + case COPY: // copying + // legacy compatibility + if (oparrayList != null && oparrayList.size() != 0) { + oparrayListList = new ArrayList<>(); + oparrayListList.add(oparrayList); + oparrayList = null; + oppatheList = new ArrayList<>(); + oppatheList.add(oppathe); + oppathe = ""; + } + for (int i = 0; i < oparrayListList.size(); i++) { + ArrayList sourceList = oparrayListList.get(i); + Intent intent1 = new Intent(this, CopyService.class); + intent1.putExtra(CopyService.TAG_COPY_SOURCES, sourceList); + intent1.putExtra(CopyService.TAG_COPY_TARGET, oppatheList.get(i)); + ServiceWatcherUtil.runService(this, intent1); + } + break; + case MOVE: // moving + // legacy compatibility + if (oparrayList != null && oparrayList.size() != 0) { + oparrayListList = new ArrayList<>(); + oparrayListList.add(oparrayList); + oparrayList = null; + oppatheList = new ArrayList<>(); + oppatheList.add(oppathe); + oppathe = ""; + } + + TaskKt.fromTask( + new MoveFilesTask( + oparrayListList, + isRootExplorer(), + mainFragment.getCurrentPath(), + this, + OpenMode.FILE, + oppatheList)); + break; + case NEW_FOLDER: // mkdir + mainActivityHelper.mkDir( + new HybridFile(OpenMode.FILE, oppathe), + RootHelper.generateBaseFile(new File(oppathe), true), + mainFragment); + break; + case RENAME: + mainActivityHelper.rename( + mainFragment.getMainFragmentViewModel().getOpenMode(), + (oppathe), + (oppathe1), + null, + false, + mainActivity, + isRootExplorer()); + mainFragment.updateList(false); + break; + case NEW_FILE: + mainActivityHelper.mkFile( + new HybridFile(OpenMode.FILE, oppathe), + new HybridFile(OpenMode.FILE, oppathe), + mainFragment); + break; + case EXTRACT: + mainActivityHelper.extractFile(new File(oppathe)); + break; + case COMPRESS: + mainActivityHelper.compressFiles(new File(oppathe), oparrayList); + break; + case SAVE_FILE: + FileUtil.writeUriToStorage( + this, urisToBeSaved, getContentResolver(), mainFragment.getCurrentPath()); + urisToBeSaved = null; + finish(); + break; + default: + LogHelper.logOnProductionOrCrash("Incorrect value for switch"); + } + return null; + }, + true); + operation = UNDEFINED; + } else if (requestCode == REQUEST_CODE_SAF) { + executeWithMainFragment( + mainFragment -> { + if (responseCode == Activity.RESULT_OK && intent.getData() != null) { + // otg access + Uri usbOtgRoot = intent.getData(); + SingletonUsbOtg.getInstance().setUsbOtgRoot(usbOtgRoot); + mainFragment.loadlist(OTGUtil.PREFIX_OTG, false, OpenMode.OTG, true); + drawer.closeIfNotLocked(); + if (drawer.isLocked()) drawer.onDrawerClosed(); + } else if (requestCode == REQUEST_CODE_SAF_FTP) { + FtpServerFragment ftpServerFragment = (FtpServerFragment) getFragmentAtFrame(); + ftpServerFragment.changeFTPServerPath(intent.getData().toString()); + Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); + + } else { + Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show(); + // otg access not provided + drawer.resetPendingPath(); + } + return null; + }, + true); + } + } + + void initialisePreferences() { + currentTab = getCurrentTab(); + skinStatusBar = PreferenceUtils.getStatusColor(getPrimary()); + } + + void initialiseViews() { - TaskKt.fromTask( - new MoveFilesTask( - oparrayListList, - isRootExplorer(), - mainFragment.getCurrentPath(), + appbar = + new AppBar( this, - OpenMode.FILE, - oppatheList)); - break; - case NEW_FOLDER: // mkdir - mainActivityHelper.mkDir( - new HybridFile(OpenMode.FILE, oppathe), - RootHelper.generateBaseFile(new File(oppathe), true), - mainFragment); - break; - case RENAME: - mainActivityHelper.rename( - mainFragment.getMainFragmentViewModel().getOpenMode(), - (oppathe), - (oppathe1), - null, - false, - mainActivity, - isRootExplorer()); - mainFragment.updateList(false); + getPrefs(), + queue -> { + if (!queue.isEmpty()) { + mainActivityHelper.search(getPrefs(), queue); + } + }); + appBarLayout = getAppbar().getAppbarLayout(); + + setSupportActionBar(getAppbar().getToolbar()); + drawer = new Drawer(this); + + indicator_layout = findViewById(R.id.indicator_layout); + + getSupportActionBar().setDisplayShowTitleEnabled(false); + fabBgView = findViewById(R.id.fabs_overlay_layout); + + switch (getAppTheme().getSimpleTheme(this)) { + case DARK: + fabBgView.setBackgroundResource(R.drawable.fab_shadow_dark); break; - case NEW_FILE: - mainActivityHelper.mkFile( - new HybridFile(OpenMode.FILE, oppathe), - new HybridFile(OpenMode.FILE, oppathe), - mainFragment); + case BLACK: + fabBgView.setBackgroundResource(R.drawable.fab_shadow_black); break; - case EXTRACT: - mainActivityHelper.extractFile(new File(oppathe)); + } + + fabBgView.setOnClickListener( + view -> { + if (getAppbar().getSearchView().isEnabled()) + getAppbar().getSearchView().hideSearchView(); + }); + + drawer.setDrawerHeaderBackground(); + } + + /** + * Call this method when you need to update the MainActivity view components' colors based on + * update in the {@link MainActivity#currentTab} Warning - All the variables should be initialised + * before calling this method! + */ + public void updateViews(ColorDrawable colorDrawable) { + // appbar view color + appbar.getBottomBar().setBackgroundColor(colorDrawable.getColor()); + // action bar color + mainActivity.getSupportActionBar().setBackgroundDrawable(colorDrawable); + + drawer.setBackgroundColor(colorDrawable.getColor()); + + if (SDK_INT >= LOLLIPOP) { + // for lollipop devices, the status bar color + mainActivity.getWindow().setStatusBarColor(colorDrawable.getColor()); + if (getBoolean(PREFERENCE_COLORED_NAVIGATION)) { + mainActivity + .getWindow() + .setNavigationBarColor(PreferenceUtils.getStatusColor(colorDrawable.getColor())); + } else { + if (getAppTheme().equals(AppTheme.LIGHT)) { + mainActivity + .getWindow() + .setNavigationBarColor(Utils.getColor(this, android.R.color.white)); + } else if (getAppTheme().equals(AppTheme.BLACK)) { + mainActivity + .getWindow() + .setNavigationBarColor(Utils.getColor(this, android.R.color.black)); + } else { + mainActivity + .getWindow() + .setNavigationBarColor(Utils.getColor(this, R.color.holo_dark_background)); + } + } + } else if (SDK_INT == KITKAT_WATCH || SDK_INT == KITKAT) { + + // for kitkat devices, the status bar color + SystemBarTintManager tintManager = new SystemBarTintManager(this); + tintManager.setStatusBarTintEnabled(true); + tintManager.setStatusBarTintColor(colorDrawable.getColor()); + } + } + + void initialiseFab() { + int colorAccent = getAccent(); + + floatingActionButton = findViewById(R.id.fabs_menu); + floatingActionButton.setMainFabClosedBackgroundColor(colorAccent); + floatingActionButton.setMainFabOpenedBackgroundColor(colorAccent); + initializeFabActionViews(); + } + + public void initializeFabActionViews() { + // NOTE: SpeedDial inverts insert index than FABsmenu + FabWithLabelView cloudFab = + initFabTitle( + R.id.menu_new_cloud, R.string.cloud_connection, R.drawable.ic_cloud_white_24dp); + FabWithLabelView newFileFab = + initFabTitle(R.id.menu_new_file, R.string.file, R.drawable.ic_insert_drive_file_white_48dp); + FabWithLabelView newFolderFab = + initFabTitle(R.id.menu_new_folder, R.string.folder, R.drawable.folder_fab); + + floatingActionButton.setOnActionSelectedListener(new FabActionListener(this)); + floatingActionButton.setOnClickListener( + view -> { + fabButtonClick(cloudFab); + }); + floatingActionButton.setOnFocusChangeListener(new CustomZoomFocusChange()); + floatingActionButton.getMainFab().setOnFocusChangeListener(new CustomZoomFocusChange()); + floatingActionButton.setNextFocusUpId(cloudFab.getId()); + floatingActionButton.getMainFab().setNextFocusUpId(cloudFab.getId()); + floatingActionButton.setOnKeyListener( + (v, keyCode, event) -> { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { + if (getCurrentTab() == 0 && getFAB().isFocused()) { + getTabFragment().setCurrentItem(1); + } + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { + findViewById(R.id.content_frame).requestFocus(); + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) { + if (pasteHelper != null + && pasteHelper.getSnackbar() != null + && pasteHelper.getSnackbar().isShown()) + ((Snackbar.SnackbarLayout) pasteHelper.getSnackbar().getView()) + .findViewById(R.id.snackBarActionButton) + .requestFocus(); + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) { + fabButtonClick(cloudFab); + } else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + onBackPressed(); + } else { + return false; + } + } + return true; + }); + cloudFab.setNextFocusDownId(floatingActionButton.getMainFab().getId()); + cloudFab.setNextFocusUpId(newFileFab.getId()); + cloudFab.setOnFocusChangeListener(new CustomZoomFocusChange()); + newFileFab.setNextFocusDownId(cloudFab.getId()); + newFileFab.setNextFocusUpId(newFolderFab.getId()); + newFileFab.setOnFocusChangeListener(new CustomZoomFocusChange()); + newFolderFab.setNextFocusDownId(newFileFab.getId()); + newFolderFab.setOnFocusChangeListener(new CustomZoomFocusChange()); + } + + private void fabButtonClick(FabWithLabelView cloudFab) { + if (floatingActionButton.isOpen()) { + floatingActionButton.close(true); + } else { + floatingActionButton.open(true); + cloudFab.requestFocus(); + } + } + + private FabWithLabelView initFabTitle( + @IdRes int id, @StringRes int fabTitle, @DrawableRes int icon) { + int iconSkin = getCurrentColorPreference().getIconSkin(); + + SpeedDialActionItem.Builder builder = + new SpeedDialActionItem.Builder(id, icon) + .setLabel(fabTitle) + .setFabBackgroundColor(iconSkin); + + switch (getAppTheme().getSimpleTheme(this)) { + case LIGHT: + fabBgView.setBackgroundResource(R.drawable.fab_shadow_light); break; - case COMPRESS: - mainActivityHelper.compressFiles(new File(oppathe), oparrayList); + case DARK: + builder + .setLabelBackgroundColor(Utils.getColor(this, R.color.holo_dark_background)) + .setLabelColor(Utils.getColor(this, R.color.text_dark)); + fabBgView.setBackgroundResource(R.drawable.fab_shadow_dark); break; - case SAVE_FILE: - FileUtil.writeUriToStorage( - this, urisToBeSaved, getContentResolver(), mainFragment.getCurrentPath()); - urisToBeSaved = null; - finish(); + case BLACK: + builder + .setLabelBackgroundColor(Color.BLACK) + .setLabelColor(Utils.getColor(this, R.color.text_dark)); + fabBgView.setBackgroundResource(R.drawable.fab_shadow_black); break; - default: - LogHelper.logOnProductionOrCrash("Incorrect value for switch"); + } + + return floatingActionButton.addActionItem(builder.create()); + } + + public boolean copyToClipboard(Context context, String text) { + try { + android.content.ClipboardManager clipboard = + (android.content.ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); + android.content.ClipData clip = + android.content.ClipData.newPlainText("Path copied to clipboard", text); + clipboard.setPrimaryClip(clip); + return true; + } catch (Exception e) { + return false; + } + } + + public void renameBookmark(final String title, final String path) { + if (dataUtils.containsBooks(new String[]{title, path}) != -1) { + RenameBookmark renameBookmark = RenameBookmark.getInstance(title, path, getAccent()); + if (renameBookmark != null) renameBookmark.show(getFragmentManager(), "renamedialog"); + } + } + + public PasteHelper getPaste() { + return pasteHelper; + } + + public MainActivityActionMode getActionModeHelper() { + return this.mainActivityActionMode; + } + + public void setPaste(PasteHelper p) { + pasteHelper = p; + } + + @Override + public void onNewIntent(Intent i) { + super.onNewIntent(i); + intent = i; + path = i.getStringExtra("path"); + + if (path != null) { + if (new File(path).isDirectory()) { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment != null) { + mainFragment.loadlist(path, false, OpenMode.FILE, true); + } else { + goToMain(path); + } + } else FileUtils.openFile(new File(path), mainActivity, getPrefs()); + } else if (i.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { + ArrayList failedOps = + i.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); + if (failedOps != null) { + mainActivityHelper.showFailedOperationDialog(failedOps, this); + } + } else if (i.getCategories() != null + && i.getCategories().contains(CLOUD_AUTHENTICATOR_GDRIVE)) { + // we used an external authenticator instead of APIs. Probably for Google Drive + CloudRail.setAuthenticationResponse(intent); + if (intent.getAction() != null) { + checkForExternalIntent(intent); + invalidateFragmentAndBundle(null, false); } - return null; - }, - true); - operation = UNDEFINED; - } else if (requestCode == REQUEST_CODE_SAF) { - executeWithMainFragment( - mainFragment -> { - if (responseCode == Activity.RESULT_OK && intent.getData() != null) { - // otg access - Uri usbOtgRoot = intent.getData(); - SingletonUsbOtg.getInstance().setUsbOtgRoot(usbOtgRoot); - mainFragment.loadlist(OTGUtil.PREFIX_OTG, false, OpenMode.OTG, true); - drawer.closeIfNotLocked(); - if (drawer.isLocked()) drawer.onDrawerClosed(); - } else if (requestCode == REQUEST_CODE_SAF_FTP) { - FtpServerFragment ftpServerFragment = (FtpServerFragment) getFragmentAtFrame(); - ftpServerFragment.changeFTPServerPath(intent.getData().toString()); - Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); + } else if ((openProcesses = i.getBooleanExtra(KEY_INTENT_PROCESS_VIEWER, false))) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace( + R.id.content_frame, new ProcessViewerFragment(), KEY_INTENT_PROCESS_VIEWER); + // transaction.addToBackStack(null); + openProcesses = false; + // title.setText(utils.getString(con, R.string.process_viewer)); + // Commit the transaction + transaction.commitAllowingStateLoss(); + supportInvalidateOptionsMenu(); + } else if (intent.getAction() != null) { + checkForExternalIntent(intent); + invalidateFragmentAndBundle(null, false); + + if (SDK_INT >= KITKAT) { + if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + SingletonUsbOtg.getInstance().resetUsbOtgRoot(); + drawer.refreshDrawer(); + } + } + } + } + + private BroadcastReceiver receiver2 = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent i) { + if (i.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { + ArrayList failedOps = + i.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); + if (failedOps != null) { + mainActivityHelper.showFailedOperationDialog(failedOps, mainActivity); + } + } + } + }; + + public void showSMBDialog(String name, String path, boolean edit) { + if (path.length() > 0 && name.length() == 0) { + int i = dataUtils.containsServer(new String[]{name, path}); + if (i != -1) name = dataUtils.getServers().get(i)[0]; + } + SmbConnectDialog smbConnectDialog = new SmbConnectDialog(); + Bundle bundle = new Bundle(); + bundle.putString("name", name); + bundle.putString("path", path); + bundle.putBoolean("edit", edit); + smbConnectDialog.setArguments(bundle); + smbConnectDialog.show(getFragmentManager(), "smbdailog"); + } + + @SuppressLint("CheckResult") + public void showSftpDialog(String name, String path, boolean edit) { + if (path.length() > 0 && name.length() == 0) { + int i = dataUtils.containsServer(new String[]{name, path}); + if (i != -1) name = dataUtils.getServers().get(i)[0]; + } + SftpConnectDialog sftpConnectDialog = new SftpConnectDialog(); + String finalName = name; + Flowable.fromCallable(() -> new NetCopyClientConnectionPool.ConnectionInfo(path)) + .flatMap( + connectionInfo -> { + Bundle retval = new Bundle(); + retval.putString(ARG_PROTOCOL, connectionInfo.getPrefix()); + retval.putString(ARG_NAME, finalName); + retval.putString(ARG_ADDRESS, connectionInfo.getHost()); + retval.putInt(ARG_PORT, connectionInfo.getPort()); + if (!TextUtils.isEmpty(connectionInfo.getDefaultPath())) { + retval.putString(ARG_DEFAULT_PATH, connectionInfo.getDefaultPath()); + } + retval.putString(ARG_USERNAME, connectionInfo.getUsername()); + + if (connectionInfo.getPassword() == null) { + retval.putBoolean(ARG_HAS_PASSWORD, false); + retval.putString(ARG_KEYPAIR_NAME, utilsHandler.getSshAuthPrivateKeyName(path)); + } else { + retval.putBoolean(ARG_HAS_PASSWORD, true); + retval.putString(ARG_PASSWORD, connectionInfo.getPassword()); + } + retval.putBoolean(ARG_EDIT, edit); + return Flowable.just(retval); + }) + .subscribeOn(Schedulers.computation()) + .subscribe( + bundle -> { + sftpConnectDialog.setArguments(bundle); + sftpConnectDialog.setCancelable(true); + sftpConnectDialog.show(getSupportFragmentManager(), "sftpdialog"); + }); + } + /** + * Shows a view that goes from white at it's lowest part to transparent a the top. It covers the + * fragment. + */ + public void showSmokeScreen() { + fabBgView.show(); + } + + public void hideSmokeScreen() { + fabBgView.hide(); + } + + @Override + @SuppressLint("CheckResult") + public void addConnection( + boolean edit, + @NonNull final String name, + @NonNull final String path, + @Nullable final String encryptedPath, + @Nullable final String oldname, + @Nullable final String oldPath) { + String[] s = new String[]{name, path}; + if (!edit) { + if ((dataUtils.containsServer(path)) == -1) { + Completable.fromRunnable( + () -> { + utilsHandler.saveToDatabase( + new OperationData(UtilsHandler.Operation.SMB, name, encryptedPath)); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> { + dataUtils.addServer(s); + drawer.refreshDrawer(); + // grid.addPath(name, encryptedPath, DataUtils.SMB, 1); + executeWithMainFragment( + mainFragment -> { + mainFragment.loadlist(path, false, OpenMode.UNKNOWN, true); + return null; + }, + true); + }); } else { - Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show(); - // otg access not provided - drawer.resetPendingPath(); + Snackbar.make( + findViewById(R.id.navigation), + getString(R.string.connection_exists), + Snackbar.LENGTH_SHORT) + .show(); } - return null; - }, - true); - } - } - - void initialisePreferences() { - currentTab = getCurrentTab(); - skinStatusBar = PreferenceUtils.getStatusColor(getPrimary()); - } - - void initialiseViews() { - - appbar = - new AppBar( - this, - getPrefs(), - queue -> { - if (!queue.isEmpty()) { - mainActivityHelper.search(getPrefs(), queue); - } - }); - appBarLayout = getAppbar().getAppbarLayout(); - - setSupportActionBar(getAppbar().getToolbar()); - drawer = new Drawer(this); - - indicator_layout = findViewById(R.id.indicator_layout); - - getSupportActionBar().setDisplayShowTitleEnabled(false); - fabBgView = findViewById(R.id.fabs_overlay_layout); - - switch (getAppTheme().getSimpleTheme(this)) { - case DARK: - fabBgView.setBackgroundResource(R.drawable.fab_shadow_dark); - break; - case BLACK: - fabBgView.setBackgroundResource(R.drawable.fab_shadow_black); - break; - } - - fabBgView.setOnClickListener( - view -> { - if (getAppbar().getSearchView().isEnabled()) getAppbar().getSearchView().hideSearchView(); - }); - - drawer.setDrawerHeaderBackground(); - } - - /** - * Call this method when you need to update the MainActivity view components' colors based on - * update in the {@link MainActivity#currentTab} Warning - All the variables should be initialised - * before calling this method! - */ - public void updateViews(ColorDrawable colorDrawable) { - // appbar view color - appbar.getBottomBar().setBackgroundColor(colorDrawable.getColor()); - // action bar color - mainActivity.getSupportActionBar().setBackgroundDrawable(colorDrawable); - - drawer.setBackgroundColor(colorDrawable.getColor()); - - if (SDK_INT >= LOLLIPOP) { - // for lollipop devices, the status bar color - mainActivity.getWindow().setStatusBarColor(colorDrawable.getColor()); - if (getBoolean(PREFERENCE_COLORED_NAVIGATION)) { - mainActivity - .getWindow() - .setNavigationBarColor(PreferenceUtils.getStatusColor(colorDrawable.getColor())); - } else { - if (getAppTheme().equals(AppTheme.LIGHT)) { - mainActivity - .getWindow() - .setNavigationBarColor(Utils.getColor(this, android.R.color.white)); - } else if (getAppTheme().equals(AppTheme.BLACK)) { - mainActivity - .getWindow() - .setNavigationBarColor(Utils.getColor(this, android.R.color.black)); } else { - mainActivity - .getWindow() - .setNavigationBarColor(Utils.getColor(this, R.color.holo_dark_background)); + int i = dataUtils.containsServer(new String[]{oldname, oldPath}); + if (i != -1) { + dataUtils.removeServer(i); + + AppConfig.getInstance() + .runInBackground( + () -> { + utilsHandler.renameSMB(oldname, oldPath, name, path); + }); + // mainActivity.grid.removePath(oldname, oldPath, DataUtils.SMB); + } + dataUtils.addServer(s); + Collections.sort(dataUtils.getServers(), new BookSorter()); + drawer.refreshDrawer(); + // mainActivity.grid.addPath(name, encryptedPath, DataUtils.SMB, 1); } - } - } else if (SDK_INT == KITKAT_WATCH || SDK_INT == KITKAT) { - - // for kitkat devices, the status bar color - SystemBarTintManager tintManager = new SystemBarTintManager(this); - tintManager.setStatusBarTintEnabled(true); - tintManager.setStatusBarTintColor(colorDrawable.getColor()); - } - } - - void initialiseFab() { - int colorAccent = getAccent(); - - floatingActionButton = findViewById(R.id.fabs_menu); - floatingActionButton.setMainFabClosedBackgroundColor(colorAccent); - floatingActionButton.setMainFabOpenedBackgroundColor(colorAccent); - initializeFabActionViews(); - } - - public void initializeFabActionViews() { - // NOTE: SpeedDial inverts insert index than FABsmenu - FabWithLabelView cloudFab = - initFabTitle( - R.id.menu_new_cloud, R.string.cloud_connection, R.drawable.ic_cloud_white_24dp); - FabWithLabelView newFileFab = - initFabTitle(R.id.menu_new_file, R.string.file, R.drawable.ic_insert_drive_file_white_48dp); - FabWithLabelView newFolderFab = - initFabTitle(R.id.menu_new_folder, R.string.folder, R.drawable.folder_fab); - - floatingActionButton.setOnActionSelectedListener(new FabActionListener(this)); - floatingActionButton.setOnClickListener( - view -> { - fabButtonClick(cloudFab); - }); - floatingActionButton.setOnFocusChangeListener(new CustomZoomFocusChange()); - floatingActionButton.getMainFab().setOnFocusChangeListener(new CustomZoomFocusChange()); - floatingActionButton.setNextFocusUpId(cloudFab.getId()); - floatingActionButton.getMainFab().setNextFocusUpId(cloudFab.getId()); - floatingActionButton.setOnKeyListener( - (v, keyCode, event) -> { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { - if (getCurrentTab() == 0 && getFAB().isFocused()) { - getTabFragment().setCurrentItem(1); - } - } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { - findViewById(R.id.content_frame).requestFocus(); - } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) { - if (pasteHelper != null - && pasteHelper.getSnackbar() != null - && pasteHelper.getSnackbar().isShown()) - ((Snackbar.SnackbarLayout) pasteHelper.getSnackbar().getView()) - .findViewById(R.id.snackBarActionButton) - .requestFocus(); - } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) { - fabButtonClick(cloudFab); - } else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { - onBackPressed(); + } + + @Override + @SuppressLint("CheckResult") + public void deleteConnection(final String name, final String path) { + int i = dataUtils.containsServer(new String[]{name, path}); + if (i != -1) { + dataUtils.removeServer(i); + Completable.fromCallable( + () -> { + utilsHandler.removeFromDatabase( + new OperationData(UtilsHandler.Operation.SMB, name, path)); + return true; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> drawer.refreshDrawer()); + } + } + + @Override + @SuppressLint("CheckResult") + public void delete(String title, String path) { + Completable.fromCallable( + () -> { + utilsHandler.removeFromDatabase( + new OperationData(UtilsHandler.Operation.BOOKMARKS, title, path)); + return true; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> drawer.refreshDrawer()); + } + + @Override + @SuppressLint("CheckResult") + public void modify(String oldpath, String oldname, String newPath, String newname) { + Completable.fromCallable( + () -> { + utilsHandler.renameBookmark(oldname, oldpath, newname, newPath); + return true; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> drawer.refreshDrawer()); + } + + @Override + public void onPreExecute(String query) { + executeWithMainFragment( + mainFragment -> { + mainFragment.mSwipeRefreshLayout.setRefreshing(true); + mainFragment.onSearchPreExecute(query); + return null; + }); + } + + @Override + public void onPostExecute(String query) { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment == null) { + // TODO cancel search + return; + } + + mainFragment.onSearchCompleted(query); + mainFragment.mSwipeRefreshLayout.setRefreshing(false); + } + + @Override + public void onProgressUpdate(@NonNull HybridFileParcelable hybridFileParcelable, String query) { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment == null) { + // TODO cancel search + return; + } + + mainFragment.addSearchResult(hybridFileParcelable, query); + } + + @Override + public void onCancelled() { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment == null) { + return; + } + + mainFragment.reloadListElements( + false, false, !mainFragment.getMainFragmentViewModel().isList()); + mainFragment.mSwipeRefreshLayout.setRefreshing(false); + } + + @Override + public void addConnection(OpenMode service) { + try { + if (cloudHandler.findEntry(service) != null) { + // cloud entry already exists + Toast.makeText( + this, getResources().getString(R.string.connection_exists), Toast.LENGTH_LONG) + .show(); + } else if (BuildConfig.IS_VERSION_FDROID) { + Toast.makeText( + this, getResources().getString(R.string.cloud_error_fdroid), Toast.LENGTH_LONG) + .show(); } else { - return false; + Toast.makeText( + MainActivity.this, + getResources().getString(R.string.please_wait), + Toast.LENGTH_LONG) + .show(); + Bundle args = new Bundle(); + args.putInt(ARGS_KEY_LOADER, service.ordinal()); + + // check if we already had done some work on the loader + Loader loader = getSupportLoaderManager().getLoader(REQUEST_CODE_CLOUD_LIST_KEY); + if (loader != null && loader.isStarted()) { + + // making sure that loader is not started + getSupportLoaderManager().destroyLoader(REQUEST_CODE_CLOUD_LIST_KEY); + } + + getSupportLoaderManager().initLoader(REQUEST_CODE_CLOUD_LIST_KEY, args, this); } - } - return true; - }); - cloudFab.setNextFocusDownId(floatingActionButton.getMainFab().getId()); - cloudFab.setNextFocusUpId(newFileFab.getId()); - cloudFab.setOnFocusChangeListener(new CustomZoomFocusChange()); - newFileFab.setNextFocusDownId(cloudFab.getId()); - newFileFab.setNextFocusUpId(newFolderFab.getId()); - newFileFab.setOnFocusChangeListener(new CustomZoomFocusChange()); - newFolderFab.setNextFocusDownId(newFileFab.getId()); - newFolderFab.setOnFocusChangeListener(new CustomZoomFocusChange()); - } - - private void fabButtonClick(FabWithLabelView cloudFab) { - if (floatingActionButton.isOpen()) { - floatingActionButton.close(true); - } else { - floatingActionButton.open(true); - cloudFab.requestFocus(); - } - } - - private FabWithLabelView initFabTitle( - @IdRes int id, @StringRes int fabTitle, @DrawableRes int icon) { - int iconSkin = getCurrentColorPreference().getIconSkin(); - - SpeedDialActionItem.Builder builder = - new SpeedDialActionItem.Builder(id, icon) - .setLabel(fabTitle) - .setFabBackgroundColor(iconSkin); - - switch (getAppTheme().getSimpleTheme(this)) { - case LIGHT: - fabBgView.setBackgroundResource(R.drawable.fab_shadow_light); - break; - case DARK: - builder - .setLabelBackgroundColor(Utils.getColor(this, R.color.holo_dark_background)) - .setLabelColor(Utils.getColor(this, R.color.text_dark)); - fabBgView.setBackgroundResource(R.drawable.fab_shadow_dark); - break; - case BLACK: - builder - .setLabelBackgroundColor(Color.BLACK) - .setLabelColor(Utils.getColor(this, R.color.text_dark)); - fabBgView.setBackgroundResource(R.drawable.fab_shadow_black); - break; - } - - return floatingActionButton.addActionItem(builder.create()); - } - - public boolean copyToClipboard(Context context, String text) { - try { - android.content.ClipboardManager clipboard = - (android.content.ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); - android.content.ClipData clip = - android.content.ClipData.newPlainText("Path copied to clipboard", text); - clipboard.setPrimaryClip(clip); - return true; - } catch (Exception e) { - return false; - } - } - - public void renameBookmark(final String title, final String path) { - if (dataUtils.containsBooks(new String[] {title, path}) != -1) { - RenameBookmark renameBookmark = RenameBookmark.getInstance(title, path, getAccent()); - if (renameBookmark != null) renameBookmark.show(getFragmentManager(), "renamedialog"); - } - } - - public PasteHelper getPaste() { - return pasteHelper; - } - - public MainActivityActionMode getActionModeHelper() { - return this.mainActivityActionMode; - } - - public void setPaste(PasteHelper p) { - pasteHelper = p; - } - - @Override - public void onNewIntent(Intent i) { - super.onNewIntent(i); - intent = i; - path = i.getStringExtra("path"); - - if (path != null) { - if (new File(path).isDirectory()) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment != null) { - mainFragment.loadlist(path, false, OpenMode.FILE, true); + } catch (CloudPluginException e) { + LOG.warn("failure when adding cloud plugin connections", e); + Toast.makeText(this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) + .show(); + } + } + + @Override + public void deleteConnection(OpenMode service) { + cloudHandler.clear(service); + dataUtils.removeAccount(service); + + runOnUiThread(drawer::refreshDrawer); + } + + @NonNull + @Override + public Loader onCreateLoader(int id, Bundle args) { + Uri uri = + Uri.withAppendedPath( + Uri.parse("content://" + CloudContract.PROVIDER_AUTHORITY), "/keys.db/secret_keys"); + + String[] projection = + new String[]{ + CloudContract.COLUMN_ID, + CloudContract.COLUMN_CLIENT_ID, + CloudContract.COLUMN_CLIENT_SECRET_KEY + }; + + switch (id) { + case REQUEST_CODE_CLOUD_LIST_KEY: + Uri uriAppendedPath = uri; + switch (OpenMode.getOpenMode(args.getInt(ARGS_KEY_LOADER, 2))) { + case GDRIVE: + uriAppendedPath = ContentUris.withAppendedId(uri, 2); + break; + case DROPBOX: + uriAppendedPath = ContentUris.withAppendedId(uri, 3); + break; + case BOX: + uriAppendedPath = ContentUris.withAppendedId(uri, 4); + break; + case ONEDRIVE: + uriAppendedPath = ContentUris.withAppendedId(uri, 5); + break; + } + return new CursorLoader(this, uriAppendedPath, projection, null, null, null); + case REQUEST_CODE_CLOUD_LIST_KEYS: + // we need a list of all secret keys + + try { + List cloudEntries = cloudHandler.getAllEntries(); + + // we want keys for services saved in database, and the cloudrail app key which + // is at index 1 + String ids[] = new String[cloudEntries.size() + 1]; + + ids[0] = 1 + ""; + for (int i = 1; i <= cloudEntries.size(); i++) { + + // we need to get only those cloud details which user wants + switch (cloudEntries.get(i - 1).getServiceType()) { + case GDRIVE: + ids[i] = 2 + ""; + break; + case DROPBOX: + ids[i] = 3 + ""; + break; + case BOX: + ids[i] = 4 + ""; + break; + case ONEDRIVE: + ids[i] = 5 + ""; + break; + } + } + return new CursorLoader(this, uri, projection, CloudContract.COLUMN_ID, ids, null); + } catch (CloudPluginException e) { + LOG.warn("failure when fetching cloud connections", e); + Toast.makeText( + this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) + .show(); + } + default: + Uri undefinedUriAppendedPath = ContentUris.withAppendedId(uri, 7); + return new CursorLoader(this, undefinedUriAppendedPath, projection, null, null, null); + } + } + + @Override + public void onLoadFinished(Loader loader, final Cursor data) { + if (data == null) { + Toast.makeText( + this, + getResources().getString(R.string.cloud_error_failed_restart), + Toast.LENGTH_LONG) + .show(); + return; + } + + /* + * This is hack for repeated calls to onLoadFinished(), + * we take the Cursor provided to check if the function + * has already been called on it. + * + * TODO: find a fix for repeated callbacks to onLoadFinished() + */ + if (cloudCursorData != null && cloudCursorData == data) return; + cloudCursorData = data; + + if (cloudLoaderAsyncTask != null + && cloudLoaderAsyncTask.getStatus() == AsyncTask.Status.RUNNING) { + return; + } + cloudLoaderAsyncTask = new CloudLoaderAsyncTask(this, cloudHandler, cloudCursorData); + cloudLoaderAsyncTask.execute(); + } + + @Override + public void onLoaderReset(Loader loader) { + // For passing code check + } + + public void initCornersDragListener(boolean destroy, boolean shouldInvokeLeftAndRight) { + initBottomDragListener(destroy); + initLeftRightAndTopDragListeners(destroy, shouldInvokeLeftAndRight); + } + + private void initBottomDragListener(boolean destroy) { + View bottomPlaceholder = findViewById(R.id.placeholder_drag_bottom); + if (destroy) { + bottomPlaceholder.setOnDragListener(null); + bottomPlaceholder.setVisibility(View.GONE); } else { - goToMain(path); - } - } else FileUtils.openFile(new File(path), mainActivity, getPrefs()); - } else if (i.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { - ArrayList failedOps = - i.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); - if (failedOps != null) { - mainActivityHelper.showFailedOperationDialog(failedOps, this); - } - } else if (i.getCategories() != null - && i.getCategories().contains(CLOUD_AUTHENTICATOR_GDRIVE)) { - // we used an external authenticator instead of APIs. Probably for Google Drive - CloudRail.setAuthenticationResponse(intent); - if (intent.getAction() != null) { - checkForExternalIntent(intent); - invalidateFragmentAndBundle(null, false); - } - } else if ((openProcesses = i.getBooleanExtra(KEY_INTENT_PROCESS_VIEWER, false))) { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace( - R.id.content_frame, new ProcessViewerFragment(), KEY_INTENT_PROCESS_VIEWER); - // transaction.addToBackStack(null); - openProcesses = false; - // title.setText(utils.getString(con, R.string.process_viewer)); - // Commit the transaction - transaction.commitAllowingStateLoss(); - supportInvalidateOptionsMenu(); - } else if (intent.getAction() != null) { - checkForExternalIntent(intent); - invalidateFragmentAndBundle(null, false); - - if (SDK_INT >= KITKAT) { - if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { - SingletonUsbOtg.getInstance().resetUsbOtgRoot(); - drawer.refreshDrawer(); + bottomPlaceholder.setVisibility(View.VISIBLE); + bottomPlaceholder.setOnDragListener( + new TabFragmentBottomDragListener( + () -> { + getCurrentMainFragment().smoothScrollListView(false); + return null; + }, + () -> { + getCurrentMainFragment().stopSmoothScrollListView(); + return null; + })); } - } } - } - private BroadcastReceiver receiver2 = - new BroadcastReceiver() { + private void initLeftRightAndTopDragListeners(boolean destroy, boolean shouldInvokeLeftAndRight) { + TabFragment tabFragment = getTabFragment(); + tabFragment.initLeftRightAndTopDragListeners(destroy, shouldInvokeLeftAndRight); + } + + private static final class FabActionListener implements SpeedDialView.OnActionSelectedListener { + + MainActivity mainActivity; + SpeedDialView floatingActionButton; + + FabActionListener(MainActivity mainActivity) { + this.mainActivity = mainActivity; + this.floatingActionButton = mainActivity.floatingActionButton; + } + @Override - public void onReceive(Context context, Intent i) { - if (i.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { - ArrayList failedOps = - i.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); - if (failedOps != null) { - mainActivityHelper.showFailedOperationDialog(failedOps, mainActivity); + public boolean onActionSelected(SpeedDialActionItem actionItem) { + final MainFragment ma = + (MainFragment) + ((TabFragment) + mainActivity.getSupportFragmentManager().findFragmentById(R.id.content_frame)) + .getCurrentTabFragment(); + final String path = ma.getCurrentPath(); + + switch (actionItem.getId()) { + case R.id.menu_new_folder: + mainActivity.mainActivityHelper.mkdir( + ma.getMainFragmentViewModel().getOpenMode(), path, ma); + break; + case R.id.menu_new_file: + mainActivity.mainActivityHelper.mkfile( + ma.getMainFragmentViewModel().getOpenMode(), path, ma); + break; + case R.id.menu_new_cloud: + BottomSheetDialogFragment fragment = new CloudSheetFragment(); + fragment.show( + ma.getActivity().getSupportFragmentManager(), CloudSheetFragment.TAG_FRAGMENT); + break; } - } - } - }; - - public void showSMBDialog(String name, String path, boolean edit) { - if (path.length() > 0 && name.length() == 0) { - int i = dataUtils.containsServer(new String[] {name, path}); - if (i != -1) name = dataUtils.getServers().get(i)[0]; - } - SmbConnectDialog smbConnectDialog = new SmbConnectDialog(); - Bundle bundle = new Bundle(); - bundle.putString("name", name); - bundle.putString("path", path); - bundle.putBoolean("edit", edit); - smbConnectDialog.setArguments(bundle); - smbConnectDialog.show(getFragmentManager(), "smbdailog"); - } - - @SuppressLint("CheckResult") - public void showSftpDialog(String name, String path, boolean edit) { - if (path.length() > 0 && name.length() == 0) { - int i = dataUtils.containsServer(new String[] {name, path}); - if (i != -1) name = dataUtils.getServers().get(i)[0]; - } - SftpConnectDialog sftpConnectDialog = new SftpConnectDialog(); - String finalName = name; - Flowable.fromCallable(() -> new NetCopyClientConnectionPool.ConnectionInfo(path)) - .flatMap( - connectionInfo -> { - Bundle retval = new Bundle(); - retval.putString(ARG_PROTOCOL, connectionInfo.getPrefix()); - retval.putString(ARG_NAME, finalName); - retval.putString(ARG_ADDRESS, connectionInfo.getHost()); - retval.putInt(ARG_PORT, connectionInfo.getPort()); - if (!TextUtils.isEmpty(connectionInfo.getDefaultPath())) { - retval.putString(ARG_DEFAULT_PATH, connectionInfo.getDefaultPath()); - } - retval.putString(ARG_USERNAME, connectionInfo.getUsername()); - - if (connectionInfo.getPassword() == null) { - retval.putBoolean(ARG_HAS_PASSWORD, false); - retval.putString(ARG_KEYPAIR_NAME, utilsHandler.getSshAuthPrivateKeyName(path)); - } else { - retval.putBoolean(ARG_HAS_PASSWORD, true); - retval.putString(ARG_PASSWORD, connectionInfo.getPassword()); - } - retval.putBoolean(ARG_EDIT, edit); - return Flowable.just(retval); - }) - .subscribeOn(Schedulers.computation()) - .subscribe( - bundle -> { - sftpConnectDialog.setArguments(bundle); - sftpConnectDialog.setCancelable(true); - sftpConnectDialog.show(getSupportFragmentManager(), "sftpdialog"); - }); - } - - /** - * Shows a view that goes from white at it's lowest part to transparent a the top. It covers the - * fragment. - */ - public void showSmokeScreen() { - fabBgView.show(); - } - - public void hideSmokeScreen() { - fabBgView.hide(); - } - - @Override - @SuppressLint("CheckResult") - public void addConnection( - boolean edit, - @NonNull final String name, - @NonNull final String path, - @Nullable final String encryptedPath, - @Nullable final String oldname, - @Nullable final String oldPath) { - String[] s = new String[] {name, path}; - if (!edit) { - if ((dataUtils.containsServer(path)) == -1) { - Completable.fromRunnable( - () -> { - utilsHandler.saveToDatabase( - new OperationData(UtilsHandler.Operation.SMB, name, encryptedPath)); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - () -> { - dataUtils.addServer(s); - drawer.refreshDrawer(); - // grid.addPath(name, encryptedPath, DataUtils.SMB, 1); - executeWithMainFragment( - mainFragment -> { - mainFragment.loadlist(path, false, OpenMode.UNKNOWN, true); - return null; - }, - true); - }); - } else { - Snackbar.make( - findViewById(R.id.navigation), - getString(R.string.connection_exists), - Snackbar.LENGTH_SHORT) - .show(); - } - } else { - int i = dataUtils.containsServer(new String[] {oldname, oldPath}); - if (i != -1) { - dataUtils.removeServer(i); - - AppConfig.getInstance() - .runInBackground( - () -> { - utilsHandler.renameSMB(oldname, oldPath, name, path); - }); - // mainActivity.grid.removePath(oldname, oldPath, DataUtils.SMB); - } - dataUtils.addServer(s); - Collections.sort(dataUtils.getServers(), new BookSorter()); - drawer.refreshDrawer(); - // mainActivity.grid.addPath(name, encryptedPath, DataUtils.SMB, 1); - } - } - - @Override - @SuppressLint("CheckResult") - public void deleteConnection(final String name, final String path) { - int i = dataUtils.containsServer(new String[] {name, path}); - if (i != -1) { - dataUtils.removeServer(i); - Completable.fromCallable( - () -> { - utilsHandler.removeFromDatabase( - new OperationData(UtilsHandler.Operation.SMB, name, path)); - return true; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> drawer.refreshDrawer()); - } - } - - @Override - @SuppressLint("CheckResult") - public void delete(String title, String path) { - Completable.fromCallable( - () -> { - utilsHandler.removeFromDatabase( - new OperationData(UtilsHandler.Operation.BOOKMARKS, title, path)); - return true; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> drawer.refreshDrawer()); - } - - @Override - @SuppressLint("CheckResult") - public void modify(String oldpath, String oldname, String newPath, String newname) { - Completable.fromCallable( - () -> { - utilsHandler.renameBookmark(oldname, oldpath, newname, newPath); - return true; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> drawer.refreshDrawer()); - } - - @Override - public void onPreExecute(String query) { - executeWithMainFragment( - mainFragment -> { - mainFragment.mSwipeRefreshLayout.setRefreshing(true); - mainFragment.onSearchPreExecute(query); - return null; - }); - } - - @Override - public void onPostExecute(String query) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment == null) { - // TODO cancel search - return; - } - - mainFragment.onSearchCompleted(query); - mainFragment.mSwipeRefreshLayout.setRefreshing(false); - } - - @Override - public void onProgressUpdate(@NonNull HybridFileParcelable hybridFileParcelable, String query) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment == null) { - // TODO cancel search - return; - } - - mainFragment.addSearchResult(hybridFileParcelable, query); - } - - @Override - public void onCancelled() { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment == null) { - return; - } - - mainFragment.reloadListElements( - false, false, !mainFragment.getMainFragmentViewModel().isList()); - mainFragment.mSwipeRefreshLayout.setRefreshing(false); - } - - @Override - public void addConnection(OpenMode service) { - try { - if (cloudHandler.findEntry(service) != null) { - // cloud entry already exists - Toast.makeText( - this, getResources().getString(R.string.connection_exists), Toast.LENGTH_LONG) - .show(); - } else if (BuildConfig.IS_VERSION_FDROID) { - Toast.makeText( - this, getResources().getString(R.string.cloud_error_fdroid), Toast.LENGTH_LONG) - .show(); - } else { - Toast.makeText( - MainActivity.this, - getResources().getString(R.string.please_wait), - Toast.LENGTH_LONG) - .show(); - Bundle args = new Bundle(); - args.putInt(ARGS_KEY_LOADER, service.ordinal()); - - // check if we already had done some work on the loader - Loader loader = getSupportLoaderManager().getLoader(REQUEST_CODE_CLOUD_LIST_KEY); - if (loader != null && loader.isStarted()) { - - // making sure that loader is not started - getSupportLoaderManager().destroyLoader(REQUEST_CODE_CLOUD_LIST_KEY); - } - - getSupportLoaderManager().initLoader(REQUEST_CODE_CLOUD_LIST_KEY, args, this); - } - } catch (CloudPluginException e) { - LOG.warn("failure when adding cloud plugin connections", e); - Toast.makeText(this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) - .show(); - } - } - - @Override - public void deleteConnection(OpenMode service) { - cloudHandler.clear(service); - dataUtils.removeAccount(service); - - runOnUiThread(drawer::refreshDrawer); - } - - @NonNull - @Override - public Loader onCreateLoader(int id, Bundle args) { - Uri uri = - Uri.withAppendedPath( - Uri.parse("content://" + CloudContract.PROVIDER_AUTHORITY), "/keys.db/secret_keys"); - - String[] projection = - new String[] { - CloudContract.COLUMN_ID, - CloudContract.COLUMN_CLIENT_ID, - CloudContract.COLUMN_CLIENT_SECRET_KEY - }; - - switch (id) { - case REQUEST_CODE_CLOUD_LIST_KEY: - Uri uriAppendedPath = uri; - switch (OpenMode.getOpenMode(args.getInt(ARGS_KEY_LOADER, 2))) { - case GDRIVE: - uriAppendedPath = ContentUris.withAppendedId(uri, 2); - break; - case DROPBOX: - uriAppendedPath = ContentUris.withAppendedId(uri, 3); - break; - case BOX: - uriAppendedPath = ContentUris.withAppendedId(uri, 4); - break; - case ONEDRIVE: - uriAppendedPath = ContentUris.withAppendedId(uri, 5); - break; - } - return new CursorLoader(this, uriAppendedPath, projection, null, null, null); - case REQUEST_CODE_CLOUD_LIST_KEYS: - // we need a list of all secret keys - - try { - List cloudEntries = cloudHandler.getAllEntries(); - // we want keys for services saved in database, and the cloudrail app key which - // is at index 1 - String ids[] = new String[cloudEntries.size() + 1]; + floatingActionButton.close(true); + return true; + } - ids[0] = 1 + ""; - for (int i = 1; i <= cloudEntries.size(); i++) { + } - // we need to get only those cloud details which user wants - switch (cloudEntries.get(i - 1).getServiceType()) { - case GDRIVE: - ids[i] = 2 + ""; - break; - case DROPBOX: - ids[i] = 3 + ""; - break; - case BOX: - ids[i] = 4 + ""; + /** + * Invoke {@link FtpServerFragment#changeFTPServerPath(String)} to change FTP server share path. + * + * @param dialog + * @param folder selected folder + * @see FtpServerFragment#changeFTPServerPath(String) + * @see FolderChooserDialog + * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback + */ + @Override + public void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File folder) { + switch (dialog.getTag()) { + case FtpServerFragment.TAG: + FtpServerFragment ftpServerFragment = (FtpServerFragment) getFragmentAtFrame(); + if (folder.exists() && folder.isDirectory()) { + if (FileUtils.isRunningAboveStorage(folder.getAbsolutePath())) { + if (!isRootExplorer()) { + AlertDialog.show( + this, + R.string.ftp_server_root_unavailable, + R.string.error, + android.R.string.ok, + null, + false); + } else { + MaterialDialog confirmDialog = + GeneralDialogCreation.showBasicDialog( + this, + R.string.ftp_server_root_filesystem_warning, + R.string.warning, + android.R.string.ok, + android.R.string.cancel); + confirmDialog + .getActionButton(DialogAction.POSITIVE) + .setOnClickListener( + v -> { + ftpServerFragment.changeFTPServerPath(folder.getPath()); + Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT) + .show(); + confirmDialog.dismiss(); + }); + confirmDialog + .getActionButton(DialogAction.NEGATIVE) + .setOnClickListener(v -> confirmDialog.dismiss()); + confirmDialog.show(); + } + } else { + ftpServerFragment.changeFTPServerPath(folder.getPath()); + Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); + } + } else { + // try to get parent + String pathParentFilePath = folder.getParent(); + if (pathParentFilePath == null) { + dialog.dismiss(); + return; + } + File pathParentFile = new File(pathParentFilePath); + if (pathParentFile.exists() && pathParentFile.isDirectory()) { + ftpServerFragment.changeFTPServerPath(pathParentFile.getPath()); + Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); + } else { + // don't have access, print error + Toast.makeText(this, R.string.ftp_path_change_error_invalid, Toast.LENGTH_SHORT).show(); + } + } + dialog.dismiss(); break; - case ONEDRIVE: - ids[i] = 5 + ""; + default: + dialog.dismiss(); break; - } - } - return new CursorLoader(this, uri, projection, CloudContract.COLUMN_ID, ids, null); - } catch (CloudPluginException e) { - LOG.warn("failure when fetching cloud connections", e); - Toast.makeText( - this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) - .show(); } - default: - Uri undefinedUriAppendedPath = ContentUris.withAppendedId(uri, 7); - return new CursorLoader(this, undefinedUriAppendedPath, projection, null, null, null); } - } - @Override - public void onLoadFinished(Loader loader, final Cursor data) { - if (data == null) { - Toast.makeText( - this, - getResources().getString(R.string.cloud_error_failed_restart), - Toast.LENGTH_LONG) - .show(); - return; + /** + * Get whether list item is selected for action mode or not + * + * @return value + */ + public boolean getListItemSelected() { + return this.listItemSelected; } - /* - * This is hack for repeated calls to onLoadFinished(), - * we take the Cursor provided to check if the function - * has already been called on it. + public String getScrollToFileName() { + return this.scrollToFileName; + } + + /** + * Set list item selected value * - * TODO: find a fix for repeated callbacks to onLoadFinished() + * @param value value */ - if (cloudCursorData != null && cloudCursorData == data) return; - cloudCursorData = data; - - if (cloudLoaderAsyncTask != null - && cloudLoaderAsyncTask.getStatus() == AsyncTask.Status.RUNNING) { - return; - } - cloudLoaderAsyncTask = new CloudLoaderAsyncTask(this, cloudHandler, cloudCursorData); - cloudLoaderAsyncTask.execute(); - } - - @Override - public void onLoaderReset(Loader loader) { - // For passing code check - } - - public void initCornersDragListener(boolean destroy, boolean shouldInvokeLeftAndRight) { - initBottomDragListener(destroy); - initLeftRightAndTopDragListeners(destroy, shouldInvokeLeftAndRight); - } - - private void initBottomDragListener(boolean destroy) { - View bottomPlaceholder = findViewById(R.id.placeholder_drag_bottom); - if (destroy) { - bottomPlaceholder.setOnDragListener(null); - bottomPlaceholder.setVisibility(View.GONE); - } else { - bottomPlaceholder.setVisibility(View.VISIBLE); - bottomPlaceholder.setOnDragListener( - new TabFragmentBottomDragListener( - () -> { - getCurrentMainFragment().smoothScrollListView(false); - return null; - }, - () -> { - getCurrentMainFragment().stopSmoothScrollListView(); - return null; - })); - } - } - - private void initLeftRightAndTopDragListeners(boolean destroy, boolean shouldInvokeLeftAndRight) { - TabFragment tabFragment = getTabFragment(); - tabFragment.initLeftRightAndTopDragListeners(destroy, shouldInvokeLeftAndRight); - } - - private static final class FabActionListener implements SpeedDialView.OnActionSelectedListener { - - MainActivity mainActivity; - SpeedDialView floatingActionButton; - - FabActionListener(MainActivity mainActivity) { - this.mainActivity = mainActivity; - this.floatingActionButton = mainActivity.floatingActionButton; + public void setListItemSelected(boolean value) { + this.listItemSelected = value; } + /** + * Do nothing other than dismissing the folder selection dialog. + * + * @param dialog + * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback + */ @Override - public boolean onActionSelected(SpeedDialActionItem actionItem) { - final MainFragment ma = - (MainFragment) - ((TabFragment) - mainActivity.getSupportFragmentManager().findFragmentById(R.id.content_frame)) - .getCurrentTabFragment(); - final String path = ma.getCurrentPath(); - - switch (actionItem.getId()) { - case R.id.menu_new_folder: - mainActivity.mainActivityHelper.mkdir( - ma.getMainFragmentViewModel().getOpenMode(), path, ma); - break; - case R.id.menu_new_file: - mainActivity.mainActivityHelper.mkfile( - ma.getMainFragmentViewModel().getOpenMode(), path, ma); - break; - case R.id.menu_new_cloud: - BottomSheetDialogFragment fragment = new CloudSheetFragment(); - fragment.show( - ma.getActivity().getSupportFragmentManager(), CloudSheetFragment.TAG_FRAGMENT); - break; - } + public void onFolderChooserDismissed(@NonNull FolderChooserDialog dialog) { + dialog.dismiss(); + } - floatingActionButton.close(true); - return true; - } - } - /** - * Invoke {@link FtpServerFragment#changeFTPServerPath(String)} to change FTP server share path. - * - * @see FtpServerFragment#changeFTPServerPath(String) - * @see FolderChooserDialog - * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback - * @param dialog - * @param folder selected folder - */ - @Override - public void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File folder) { - switch (dialog.getTag()) { - case FtpServerFragment.TAG: - FtpServerFragment ftpServerFragment = (FtpServerFragment) getFragmentAtFrame(); - if (folder.exists() && folder.isDirectory()) { - if (FileUtils.isRunningAboveStorage(folder.getAbsolutePath())) { - if (!isRootExplorer()) { - AlertDialog.show( - this, - R.string.ftp_server_root_unavailable, - R.string.error, - android.R.string.ok, - null, - false); - } else { - MaterialDialog confirmDialog = - GeneralDialogCreation.showBasicDialog( - this, - R.string.ftp_server_root_filesystem_warning, - R.string.warning, - android.R.string.ok, - android.R.string.cancel); - confirmDialog - .getActionButton(DialogAction.POSITIVE) - .setOnClickListener( - v -> { - ftpServerFragment.changeFTPServerPath(folder.getPath()); - Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT) - .show(); - confirmDialog.dismiss(); - }); - confirmDialog - .getActionButton(DialogAction.NEGATIVE) - .setOnClickListener(v -> confirmDialog.dismiss()); - confirmDialog.show(); - } - } else { - ftpServerFragment.changeFTPServerPath(folder.getPath()); - Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); - } + private void executeWithMainFragment(@NonNull Function lambda) { + executeWithMainFragment(lambda, false); + } + + @Nullable + private void executeWithMainFragment( + @NonNull Function lambda, boolean showToastIfMainFragmentIsNull) { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment != null && mainFragment.getMainFragmentViewModel() != null) { + lambda.apply(mainFragment); } else { - // try to get parent - String pathParentFilePath = folder.getParent(); - if (pathParentFilePath == null) { - dialog.dismiss(); - return; - } - File pathParentFile = new File(pathParentFilePath); - if (pathParentFile.exists() && pathParentFile.isDirectory()) { - ftpServerFragment.changeFTPServerPath(pathParentFile.getPath()); - Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); - } else { - // don't have access, print error - Toast.makeText(this, R.string.ftp_path_change_error_invalid, Toast.LENGTH_SHORT).show(); - } + LOG.warn("MainFragment is null"); + if (showToastIfMainFragmentIsNull) { + AppConfig.toast(this, R.string.operation_unsuccesful); + } } - dialog.dismiss(); - break; - default: - dialog.dismiss(); - break; - } - } - - /** - * Get whether list item is selected for action mode or not - * - * @return value - */ - public boolean getListItemSelected() { - return this.listItemSelected; - } - - public String getScrollToFileName() { - return this.scrollToFileName; - } - - /** - * 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. - * - * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback - * @param dialog - */ - @Override - public void onFolderChooserDismissed(@NonNull FolderChooserDialog dialog) { - dialog.dismiss(); - } - - private void executeWithMainFragment(@NonNull Function lambda) { - executeWithMainFragment(lambda, false); - } - - @Nullable - private void executeWithMainFragment( - @NonNull Function lambda, boolean showToastIfMainFragmentIsNull) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment != null && mainFragment.getMainFragmentViewModel() != null) { - lambda.apply(mainFragment); - } else { - LOG.warn("MainFragment is null"); - if (showToastIfMainFragmentIsNull) { - AppConfig.toast(this, R.string.operation_unsuccesful); - } } - } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index f6b1345d13..c792d5d00b 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -236,4 +236,5 @@ private void initSearchViewColor(MainActivity a) { public interface SearchListener { void onSearch(String queue); } + } From 929ba5d78dc83635389abaf69d99d43308d455a7 Mon Sep 17 00:00:00 2001 From: peerzadaburhan Date: Thu, 2 Feb 2023 14:45:47 +0530 Subject: [PATCH 043/384] Issue #3394 --- .../ui/activities/MainActivity.java | 4316 ++++++++--------- .../ui/views/appbar/SearchView.java | 1 - 2 files changed, 2140 insertions(+), 2177 deletions(-) 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 e1f509521e..1ebb3a8b9f 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 @@ -162,7 +162,6 @@ import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Color; -import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.hardware.usb.UsbManager; import android.media.RingtoneManager; @@ -176,19 +175,12 @@ import android.os.storage.StorageVolume; import android.service.quicksettings.TileService; import android.text.TextUtils; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; 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.ViewTreeObserver; -import android.view.WindowInsets; import android.view.animation.DecelerateInterpolator; -import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import androidx.annotation.DrawableRes; @@ -199,9 +191,6 @@ import androidx.annotation.StringRes; import androidx.arch.core.util.Function; import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import androidx.loader.app.LoaderManager; @@ -216,7 +205,7 @@ import io.reactivex.schedulers.Schedulers; public class MainActivity extends PermissionsActivity - implements SmbConnectionListener, + implements SmbConnectionListener, BookmarkCallback, SearchWorkerFragment.HelperCallbacks, CloudConnectionCallbacks, @@ -224,803 +213,788 @@ public class MainActivity extends PermissionsActivity FolderChooserDialog.FolderCallback, PermissionsActivity.OnPermissionGranted { - private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); - - public static final Pattern DIR_SEPARATOR = Pattern.compile("/"); - public static final String TAG_ASYNC_HELPER = "async_helper"; - - private DataUtils dataUtils; - - public String path = ""; - public boolean mReturnIntent = false; - public boolean isCompressedOpen = false; - public boolean mRingtonePickerIntent = false; - public int skinStatusBar; - - private SpeedDialView floatingActionButton; - - public MainActivityHelper mainActivityHelper; - - public int operation = -1; - public ArrayList oparrayList; - public ArrayList> oparrayListList; - - // oppathe - the path at which certain operation needs to be performed - // oppathe1 - the new path which user wants to create/modify - // oppathList - the paths at which certain operation needs to be performed (pairs with - // oparrayList) - public String oppathe, oppathe1; - public ArrayList oppatheList; - - // This holds the Uris to be written at initFabToSave() - private ArrayList urisToBeSaved; - - public static final String PASTEHELPER_BUNDLE = "pasteHelper"; - - private static final String KEY_DRAWER_SELECTED = "selectitem"; - private static final String KEY_OPERATION_PATH = "oppathe"; - 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; - // private HistoryManager history, grid; - private MainActivity mainActivity = this; - private String pathInCompressedArchive; - private boolean openProcesses = false; - private MaterialDialog materialDialog; - private boolean backPressedToExitOnce = false; - private WeakReference toast = new WeakReference<>(null); - private Intent intent; - private View indicator_layout; - - private AppBarLayout appBarLayout; - - private SpeedDialOverlayLayout fabBgView; - private UtilsHandler utilsHandler; - private CloudHandler cloudHandler; - private CloudLoaderAsyncTask cloudLoaderAsyncTask; - /** - * This is for a hack. - * - * @see MainActivity#onLoadFinished(Loader, Cursor) - */ - private Cursor cloudCursorData = null; - - public static final int REQUEST_CODE_SAF = 223; - - public static final String KEY_INTENT_PROCESS_VIEWER = "openprocesses"; - public static final String TAG_INTENT_FILTER_FAILED_OPS = "failedOps"; - public static final String TAG_INTENT_FILTER_GENERAL = "general_communications"; - public static final String ARGS_KEY_LOADER = "loader_cloud_args_service"; - - /** - * Broadcast which will be fired after every file operation, will denote list loading Registered - * by {@link MainFragment} - */ - public static final String KEY_INTENT_LOAD_LIST = "loadlist"; - - /** - * Extras carried by the list loading intent Contains path of parent directory in which operation - * took place, so that we can run media scanner on it - */ - public static final String KEY_INTENT_LOAD_LIST_FILE = "loadlist_file"; - - /** - * Mime type in intent that apps need to pass when trying to open file manager from a specific - * directory Should be clubbed with {@link Intent#ACTION_VIEW} and send in path to open in intent - * data field - */ - public static final String ARGS_INTENT_ACTION_VIEW_MIME_FOLDER = "resource/folder"; - - public static final String ARGS_INTENT_ACTION_VIEW_APPLICATION_ALL = "application/*"; - - public static final String CLOUD_AUTHENTICATOR_GDRIVE = "android.intent.category.BROWSABLE"; - public static final String CLOUD_AUTHENTICATOR_REDIRECT_URI = "com.amaze.filemanager:/auth"; - - // the current visible tab, either 0 or 1 - public static int currentTab; - private boolean listItemSelected = false; - - private String scrollToFileName = null; - - public static final int REQUEST_CODE_CLOUD_LIST_KEYS = 5463; - public static final int REQUEST_CODE_CLOUD_LIST_KEY = 5472; - - private PasteHelper pasteHelper; - private MainActivityActionMode mainActivityActionMode; + private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); + + public static final Pattern DIR_SEPARATOR = Pattern.compile("/"); + public static final String TAG_ASYNC_HELPER = "async_helper"; + + private DataUtils dataUtils; + + public String path = ""; + public boolean mReturnIntent = false; + public boolean isCompressedOpen = false; + public boolean mRingtonePickerIntent = false; + public int skinStatusBar; + + private SpeedDialView floatingActionButton; + + public MainActivityHelper mainActivityHelper; + + public int operation = -1; + public ArrayList oparrayList; + public ArrayList> oparrayListList; + + // oppathe - the path at which certain operation needs to be performed + // oppathe1 - the new path which user wants to create/modify + // oppathList - the paths at which certain operation needs to be performed (pairs with + // oparrayList) + public String oppathe, oppathe1; + public ArrayList oppatheList; + + // This holds the Uris to be written at initFabToSave() + private ArrayList urisToBeSaved; + + public static final String PASTEHELPER_BUNDLE = "pasteHelper"; + + private static final String KEY_DRAWER_SELECTED = "selectitem"; + private static final String KEY_OPERATION_PATH = "oppathe"; + 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; + // private HistoryManager history, grid; + private MainActivity mainActivity = this; + private String pathInCompressedArchive; + private boolean openProcesses = false; + private MaterialDialog materialDialog; + private boolean backPressedToExitOnce = false; + private WeakReference toast = new WeakReference<>(null); + private Intent intent; + private View indicator_layout; - private static final String DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0"; - private static final String INTERNAL_SHARED_STORAGE = "Internal shared storage"; - private static final String INTENT_ACTION_OPEN_QUICK_ACCESS = - "com.amaze.filemanager.openQuickAccess"; - private static final String INTENT_ACTION_OPEN_RECENT = "com.amaze.filemanager.openRecent"; - private static final String INTENT_ACTION_OPEN_FTP_SERVER = "com.amaze.filemanager.openFTPServer"; - private static final String INTENT_ACTION_OPEN_APP_MANAGER = - "com.amaze.filemanager.openAppManager"; + private AppBarLayout appBarLayout; - /** - * Called when the activity is first created. - */ - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main_toolbar); - - - intent = getIntent(); - - - dataUtils = DataUtils.getInstance(); - if (savedInstanceState != null) { - listItemSelected = savedInstanceState.getBoolean(KEY_SELECTED_LIST_ITEM, false); - } - - initialisePreferences(); - initializeInteractiveShell(); + private SpeedDialOverlayLayout fabBgView; + private UtilsHandler utilsHandler; + private CloudHandler cloudHandler; + private CloudLoaderAsyncTask cloudLoaderAsyncTask; + /** + * This is for a hack. + * + * @see MainActivity#onLoadFinished(Loader, Cursor) + */ + private Cursor cloudCursorData = null; + public static final int REQUEST_CODE_SAF = 223; - dataUtils.registerOnDataChangedListener(new SaveOnDataUtilsChange(drawer)); + public static final String KEY_INTENT_PROCESS_VIEWER = "openprocesses"; + public static final String TAG_INTENT_FILTER_FAILED_OPS = "failedOps"; + public static final String TAG_INTENT_FILTER_GENERAL = "general_communications"; + public static final String ARGS_KEY_LOADER = "loader_cloud_args_service"; - AppConfig.getInstance().setMainActivityContext(this); + /** + * Broadcast which will be fired after every file operation, will denote list loading Registered + * by {@link MainFragment} + */ + public static final String KEY_INTENT_LOAD_LIST = "loadlist"; - initialiseViews(); - utilsHandler = AppConfig.getInstance().getUtilsHandler(); - cloudHandler = new CloudHandler(this, AppConfig.getInstance().getExplorerDatabase()); + /** + * Extras carried by the list loading intent Contains path of parent directory in which operation + * took place, so that we can run media scanner on it + */ + public static final String KEY_INTENT_LOAD_LIST_FILE = "loadlist_file"; - initialiseFab(); // TODO: 7/12/2017 not init when actionIntent != null - mainActivityHelper = new MainActivityHelper(this); - mainActivityActionMode = new MainActivityActionMode(new WeakReference<>(MainActivity.this)); + /** + * Mime type in intent that apps need to pass when trying to open file manager from a specific + * directory Should be clubbed with {@link Intent#ACTION_VIEW} and send in path to open in intent + * data field + */ + public static final String ARGS_INTENT_ACTION_VIEW_MIME_FOLDER = "resource/folder"; - if (CloudSheetFragment.isCloudProviderAvailable(this)) { + public static final String ARGS_INTENT_ACTION_VIEW_APPLICATION_ALL = "application/*"; - LoaderManager.getInstance(this).initLoader(REQUEST_CODE_CLOUD_LIST_KEYS, null, this); - } - - path = intent.getStringExtra("path"); - openProcesses = intent.getBooleanExtra(KEY_INTENT_PROCESS_VIEWER, false); + public static final String CLOUD_AUTHENTICATOR_GDRIVE = "android.intent.category.BROWSABLE"; + public static final String CLOUD_AUTHENTICATOR_REDIRECT_URI = "com.amaze.filemanager:/auth"; - if (intent.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { - ArrayList failedOps = - intent.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); - if (failedOps != null) { - mainActivityHelper.showFailedOperationDialog(failedOps, this); - } - } + // the current visible tab, either 0 or 1 + public static int currentTab; + private boolean listItemSelected = false; - checkForExternalIntent(intent); + private String scrollToFileName = null; - drawer.setDrawerIndicatorEnabled(); + public static final int REQUEST_CODE_CLOUD_LIST_KEYS = 5463; + public static final int REQUEST_CODE_CLOUD_LIST_KEY = 5472; + private PasteHelper pasteHelper; + private MainActivityActionMode mainActivityActionMode; + + private static final String DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0"; + private static final String INTERNAL_SHARED_STORAGE = "Internal shared storage"; + private static final String INTENT_ACTION_OPEN_QUICK_ACCESS = + "com.amaze.filemanager.openQuickAccess"; + private static final String INTENT_ACTION_OPEN_RECENT = "com.amaze.filemanager.openRecent"; + private static final String INTENT_ACTION_OPEN_FTP_SERVER = "com.amaze.filemanager.openFTPServer"; + private static final String INTENT_ACTION_OPEN_APP_MANAGER = + "com.amaze.filemanager.openAppManager"; + + /** Called when the activity is first created. */ + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_toolbar); + + intent = getIntent(); + + dataUtils = DataUtils.getInstance(); + if (savedInstanceState != null) { + listItemSelected = savedInstanceState.getBoolean(KEY_SELECTED_LIST_ITEM, false); + } + + initialisePreferences(); + initializeInteractiveShell(); + + dataUtils.registerOnDataChangedListener(new SaveOnDataUtilsChange(drawer)); + + AppConfig.getInstance().setMainActivityContext(this); + + initialiseViews(); + utilsHandler = AppConfig.getInstance().getUtilsHandler(); + cloudHandler = new CloudHandler(this, AppConfig.getInstance().getExplorerDatabase()); + + initialiseFab(); // TODO: 7/12/2017 not init when actionIntent != null + mainActivityHelper = new MainActivityHelper(this); + mainActivityActionMode = new MainActivityActionMode(new WeakReference<>(MainActivity.this)); - if (!getBoolean(PREFERENCE_BOOKMARKS_ADDED)) { - utilsHandler.addCommonBookmarks(); - getPrefs().edit().putBoolean(PREFERENCE_BOOKMARKS_ADDED, true).commit(); - } + if (CloudSheetFragment.isCloudProviderAvailable(this)) { - checkForExternalPermission(); - - Completable.fromRunnable( - () -> { - dataUtils.setHiddenFiles(utilsHandler.getHiddenFilesConcurrentRadixTree()); - dataUtils.setHistory(utilsHandler.getHistoryLinkedList()); - dataUtils.setGridfiles(utilsHandler.getGridViewList()); - dataUtils.setListfiles(utilsHandler.getListViewList()); - dataUtils.setBooks(utilsHandler.getBookmarksList()); - ArrayList servers = new ArrayList<>(); - servers.addAll(utilsHandler.getSmbList()); - servers.addAll(utilsHandler.getSftpList()); - dataUtils.setServers(servers); - - ExtensionsKt.updateAUAlias( - this, - !PackageUtils.Companion.appInstalledOrNot( - AboutActivity.PACKAGE_AMAZE_UTILS, mainActivity.getPackageManager()) - && !getBoolean( - PreferencesConstants.PREFERENCE_DISABLE_PLAYER_INTENT_FILTERS)); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - new CompletableObserver() { - @Override - public void onSubscribe(@NonNull Disposable d) { - } - - @Override - public void onComplete() { - drawer.refreshDrawer(); - invalidateFragmentAndBundle(savedInstanceState, false); - } - - @Override - public void onError(@NonNull Throwable e) { - LOG.error("Error setting up DataUtils", e); - drawer.refreshDrawer(); - invalidateFragmentAndBundle(savedInstanceState, false); - } - }); - initStatusBarResources(findViewById(R.id.drawer_layout)); + LoaderManager.getInstance(this).initLoader(REQUEST_CODE_CLOUD_LIST_KEYS, null, this); } - public void invalidateFragmentAndBundle(Bundle savedInstanceState, boolean isCloudRefresh) { - if (savedInstanceState == null) { - if (openProcesses) { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace( - R.id.content_frame, new ProcessViewerFragment(), KEY_INTENT_PROCESS_VIEWER); - // transaction.addToBackStack(null); - openProcesses = false; - // title.setText(utils.getString(con, R.string.process_viewer)); - // Commit the transaction - transaction.commit(); - supportInvalidateOptionsMenu(); - } else if (intent.getAction() != null - && (intent.getAction().equals(TileService.ACTION_QS_TILE_PREFERENCES) - || INTENT_ACTION_OPEN_FTP_SERVER.equals(intent.getAction()))) { - // tile preferences, open ftp fragment - - FragmentTransaction transaction2 = getSupportFragmentManager().beginTransaction(); - transaction2.replace(R.id.content_frame, new FtpServerFragment()); - appBarLayout - .animate() - .translationY(0) - .setInterpolator(new DecelerateInterpolator(2)) - .start(); - - drawer.deselectEverything(); - transaction2.commit(); - } else if (intent.getAction() != null - && INTENT_ACTION_OPEN_APP_MANAGER.equals(intent.getAction())) { - FragmentTransaction transaction3 = getSupportFragmentManager().beginTransaction(); - transaction3.replace(R.id.content_frame, new AppsListFragment()); - appBarLayout - .animate() - .translationY(0) - .setInterpolator(new DecelerateInterpolator(2)) - .start(); - - drawer.deselectEverything(); - transaction3.commit(); - } else { - if (path != null && path.length() > 0) { - HybridFile file = new HybridFile(OpenMode.UNKNOWN, path); - file.generateMode(MainActivity.this); - if (file.isCloudDriveFile() && dataUtils.getAccounts().size() == 0) { - // not ready to serve cloud files - goToMain(null); - } else if (file.isDirectory(MainActivity.this) && !isCloudRefresh) { - goToMain(path); - } else { - if (!isCloudRefresh) { - goToMain(null); - } - if (file.isSmb() || file.isSftp()) { - String authorisedPath = - SshClientUtils.formatPlainServerPathToAuthorised(dataUtils.getServers(), path); - file.setPath(authorisedPath); - LOG.info( - "Opening smb file from deeplink, modify plain path to authorised path {}", - authorisedPath); - } - file.openFile(this, true); - } - } else if (!isCloudRefresh) { - goToMain(null); - } - } - } else { - pasteHelper = savedInstanceState.getParcelable(PASTEHELPER_BUNDLE); - oppathe = savedInstanceState.getString(KEY_OPERATION_PATH); - oppathe1 = savedInstanceState.getString(KEY_OPERATED_ON_PATH); - oparrayList = savedInstanceState.getParcelableArrayList(KEY_OPERATIONS_PATH_LIST); - operation = savedInstanceState.getInt(KEY_OPERATION); - int selectedStorage = savedInstanceState.getInt(KEY_DRAWER_SELECTED, 0); - getDrawer().selectCorrectDrawerItem(selectedStorage); - } - } + path = intent.getStringExtra("path"); + openProcesses = intent.getBooleanExtra(KEY_INTENT_PROCESS_VIEWER, false); - @Override - public void onPermissionGranted() { - drawer.refreshDrawer(); - TabFragment tabFragment = getTabFragment(); - boolean b = getBoolean(PREFERENCE_NEED_TO_SET_HOME); - // reset home and current paths according to new storages - if (b) { - TabHandler tabHandler = TabHandler.getInstance(); - tabHandler - .clear() - .subscribe( - () -> { - if (tabFragment != null) { - tabFragment.refactorDrawerStorages(false); - Fragment main = tabFragment.getFragmentAtIndex(0); - if (main != null) - ((MainFragment) main).updateTabWithDb(tabHandler.findTab(1)); - Fragment main1 = tabFragment.getFragmentAtIndex(1); - if (main1 != null) - ((MainFragment) main1).updateTabWithDb(tabHandler.findTab(2)); - } - getPrefs().edit().putBoolean(PREFERENCE_NEED_TO_SET_HOME, false).commit(); - }); - } else { - // just refresh list - if (tabFragment != null) { - Fragment main = tabFragment.getFragmentAtIndex(0); - if (main != null) ((MainFragment) main).updateList(false); - Fragment main1 = tabFragment.getFragmentAtIndex(1); - if (main1 != null) ((MainFragment) main1).updateList(false); - } - } + if (intent.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { + ArrayList failedOps = + intent.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); + if (failedOps != null) { + mainActivityHelper.showFailedOperationDialog(failedOps, this); + } } - private void checkForExternalPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (!checkStoragePermission()) { - requestStoragePermission(this, true); + checkForExternalIntent(intent); + + drawer.setDrawerIndicatorEnabled(); + + if (!getBoolean(PREFERENCE_BOOKMARKS_ADDED)) { + utilsHandler.addCommonBookmarks(); + getPrefs().edit().putBoolean(PREFERENCE_BOOKMARKS_ADDED, true).commit(); + } + + checkForExternalPermission(); + + Completable.fromRunnable( + () -> { + dataUtils.setHiddenFiles(utilsHandler.getHiddenFilesConcurrentRadixTree()); + dataUtils.setHistory(utilsHandler.getHistoryLinkedList()); + dataUtils.setGridfiles(utilsHandler.getGridViewList()); + dataUtils.setListfiles(utilsHandler.getListViewList()); + dataUtils.setBooks(utilsHandler.getBookmarksList()); + ArrayList servers = new ArrayList<>(); + servers.addAll(utilsHandler.getSmbList()); + servers.addAll(utilsHandler.getSftpList()); + dataUtils.setServers(servers); + + ExtensionsKt.updateAUAlias( + this, + !PackageUtils.Companion.appInstalledOrNot( + AboutActivity.PACKAGE_AMAZE_UTILS, mainActivity.getPackageManager()) + && !getBoolean( + PreferencesConstants.PREFERENCE_DISABLE_PLAYER_INTENT_FILTERS)); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + new CompletableObserver() { + @Override + public void onSubscribe(@NonNull Disposable d) {} + + @Override + public void onComplete() { + drawer.refreshDrawer(); + invalidateFragmentAndBundle(savedInstanceState, false); + } + + @Override + public void onError(@NonNull Throwable e) { + LOG.error("Error setting up DataUtils", e); + drawer.refreshDrawer(); + invalidateFragmentAndBundle(savedInstanceState, false); + } + }); + initStatusBarResources(findViewById(R.id.drawer_layout)); + } + + public void invalidateFragmentAndBundle(Bundle savedInstanceState, boolean isCloudRefresh) { + if (savedInstanceState == null) { + if (openProcesses) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace( + R.id.content_frame, new ProcessViewerFragment(), KEY_INTENT_PROCESS_VIEWER); + // transaction.addToBackStack(null); + openProcesses = false; + // title.setText(utils.getString(con, R.string.process_viewer)); + // Commit the transaction + transaction.commit(); + supportInvalidateOptionsMenu(); + } else if (intent.getAction() != null + && (intent.getAction().equals(TileService.ACTION_QS_TILE_PREFERENCES) + || INTENT_ACTION_OPEN_FTP_SERVER.equals(intent.getAction()))) { + // tile preferences, open ftp fragment + + FragmentTransaction transaction2 = getSupportFragmentManager().beginTransaction(); + transaction2.replace(R.id.content_frame, new FtpServerFragment()); + appBarLayout + .animate() + .translationY(0) + .setInterpolator(new DecelerateInterpolator(2)) + .start(); + + drawer.deselectEverything(); + transaction2.commit(); + } else if (intent.getAction() != null + && INTENT_ACTION_OPEN_APP_MANAGER.equals(intent.getAction())) { + FragmentTransaction transaction3 = getSupportFragmentManager().beginTransaction(); + transaction3.replace(R.id.content_frame, new AppsListFragment()); + appBarLayout + .animate() + .translationY(0) + .setInterpolator(new DecelerateInterpolator(2)) + .start(); + + drawer.deselectEverything(); + transaction3.commit(); + } else { + if (path != null && path.length() > 0) { + HybridFile file = new HybridFile(OpenMode.UNKNOWN, path); + file.generateMode(MainActivity.this); + if (file.isCloudDriveFile() && dataUtils.getAccounts().size() == 0) { + // not ready to serve cloud files + goToMain(null); + } else if (file.isDirectory(MainActivity.this) && !isCloudRefresh) { + goToMain(path); + } else { + if (!isCloudRefresh) { + goToMain(null); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - requestAllFilesAccess(this); + if (file.isSmb() || file.isSftp()) { + String authorisedPath = + SshClientUtils.formatPlainServerPathToAuthorised(dataUtils.getServers(), path); + file.setPath(authorisedPath); + LOG.info( + "Opening smb file from deeplink, modify plain path to authorised path {}", + authorisedPath); } + file.openFile(this, true); + } + } else if (!isCloudRefresh) { + goToMain(null); } - } - - /** - * Checks for the action to take when Amaze receives an intent from external source - */ - private void checkForExternalIntent(Intent intent) { - final String actionIntent = intent.getAction(); - if (actionIntent == null) { - return; - } - - final String type = intent.getType(); - - if (actionIntent.equals(Intent.ACTION_GET_CONTENT)) { - // file picker intent - mReturnIntent = true; - Toast.makeText(this, getString(R.string.pick_a_file), Toast.LENGTH_LONG).show(); - - // disable screen rotation just for convenience purpose - // TODO: Support screen rotation when picking file - Utils.disableScreenRotation(this); - } else if (actionIntent.equals(RingtoneManager.ACTION_RINGTONE_PICKER)) { - // ringtone picker intent - mReturnIntent = true; - mRingtonePickerIntent = true; - Toast.makeText(this, getString(R.string.pick_a_file), Toast.LENGTH_LONG).show(); - - // disable screen rotation just for convenience purpose - // TODO: Support screen rotation when picking file - Utils.disableScreenRotation(this); - } else if (actionIntent.equals(Intent.ACTION_VIEW)) { - // zip viewer intent - Uri uri = intent.getData(); - - if (type != null - && (type.equals(ARGS_INTENT_ACTION_VIEW_MIME_FOLDER) - || type.equals(ARGS_INTENT_ACTION_VIEW_APPLICATION_ALL))) { - // support for syncting or intents from external apps that - // need to start file manager from a specific path - - if (uri != null) { - - path = Utils.sanitizeInput(FileUtils.fromContentUri(uri).getAbsolutePath()); - scrollToFileName = intent.getStringExtra("com.amaze.fileutilities.AFM_LOCATE_FILE_NAME"); - } else { - // no data field, open home for the tab in later processing - path = null; + } + } else { + pasteHelper = savedInstanceState.getParcelable(PASTEHELPER_BUNDLE); + oppathe = savedInstanceState.getString(KEY_OPERATION_PATH); + oppathe1 = savedInstanceState.getString(KEY_OPERATED_ON_PATH); + oparrayList = savedInstanceState.getParcelableArrayList(KEY_OPERATIONS_PATH_LIST); + operation = savedInstanceState.getInt(KEY_OPERATION); + int selectedStorage = savedInstanceState.getInt(KEY_DRAWER_SELECTED, 0); + getDrawer().selectCorrectDrawerItem(selectedStorage); + } + } + + @Override + public void onPermissionGranted() { + drawer.refreshDrawer(); + TabFragment tabFragment = getTabFragment(); + boolean b = getBoolean(PREFERENCE_NEED_TO_SET_HOME); + // reset home and current paths according to new storages + if (b) { + TabHandler tabHandler = TabHandler.getInstance(); + tabHandler + .clear() + .subscribe( + () -> { + if (tabFragment != null) { + tabFragment.refactorDrawerStorages(false); + Fragment main = tabFragment.getFragmentAtIndex(0); + if (main != null) ((MainFragment) main).updateTabWithDb(tabHandler.findTab(1)); + Fragment main1 = tabFragment.getFragmentAtIndex(1); + if (main1 != null) ((MainFragment) main1).updateTabWithDb(tabHandler.findTab(2)); } - } else if (FileUtils.isCompressedFile(Utils.sanitizeInput(uri.toString()))) { - // we don't have folder resource mime type set, supposed to be zip/rar - isCompressedOpen = true; - pathInCompressedArchive = Utils.sanitizeInput(uri.toString()); - openCompressed(pathInCompressedArchive); - } else if (uri.getPath().startsWith("/open_file")) { - /** - * Deeplink to open files directly through amaze using following format: - * http://teamamaze.xyz/open_file?path=path-to-file - */ - path = Utils.sanitizeInput(uri.getQueryParameter("path")); - } else { - LOG.warn(getString(R.string.error_cannot_find_way_open)); - } - - } else if (actionIntent.equals(Intent.ACTION_SEND)) { - if ("text/plain".equals(type)) { - initFabToSave(null); - } else { - // save a single file to filesystem - Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - ArrayList uris = new ArrayList<>(); - uris.add(uri); - initFabToSave(uris); - } - // disable screen rotation just for convenience purpose - // TODO: Support screen rotation when saving a file - Utils.disableScreenRotation(this); - - } else if (actionIntent.equals(Intent.ACTION_SEND_MULTIPLE) && type != null) { - // save multiple files to filesystem - - ArrayList arrayList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - initFabToSave(arrayList); - - // disable screen rotation just for convenience purpose - // TODO: Support screen rotation when saving a file - Utils.disableScreenRotation(this); - } + getPrefs().edit().putBoolean(PREFERENCE_NEED_TO_SET_HOME, false).commit(); + }); + } else { + // just refresh list + if (tabFragment != null) { + Fragment main = tabFragment.getFragmentAtIndex(0); + if (main != null) ((MainFragment) main).updateList(false); + Fragment main1 = tabFragment.getFragmentAtIndex(1); + if (main1 != null) ((MainFragment) main1).updateList(false); + } } + } - /** - * Initializes the floating action button to act as to save data from an external intent - */ - private void initFabToSave(final ArrayList uris) { - Utils.showThemedSnackbar( - this, - getString(R.string.select_save_location), - BaseTransientBottomBar.LENGTH_INDEFINITE, - R.string.save, - () -> saveExternalIntent(uris)); + private void checkForExternalPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (!checkStoragePermission()) { + requestStoragePermission(this, true); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + requestAllFilesAccess(this); + } } + } - private void saveExternalIntent(final ArrayList uris) { - executeWithMainFragment( - mainFragment -> { - if (uris != null && uris.size() > 0) { - if (SDK_INT >= LOLLIPOP) { - File folder = new File(mainFragment.getCurrentPath()); - int result = mainActivityHelper.checkFolder(folder, MainActivity.this); - if (result == WRITABLE_OR_ON_SDCARD) { - FileUtil.writeUriToStorage( - MainActivity.this, uris, getContentResolver(), mainFragment.getCurrentPath()); - finish(); - } else { - // Trigger SAF intent, keep uri until finish - operation = SAVE_FILE; - urisToBeSaved = uris; - mainActivityHelper.checkFolder(folder, MainActivity.this); - } - } else { - FileUtil.writeUriToStorage( - MainActivity.this, uris, getContentResolver(), mainFragment.getCurrentPath()); - } - } else { - saveExternalIntentExtras(); - } - Toast.makeText( - MainActivity.this, - getResources().getString(R.string.saving) - + " to " - + mainFragment.getCurrentPath(), - Toast.LENGTH_LONG) - .show(); - finish(); - return null; - }); + /** Checks for the action to take when Amaze receives an intent from external source */ + private void checkForExternalIntent(Intent intent) { + final String actionIntent = intent.getAction(); + if (actionIntent == null) { + return; } - private void saveExternalIntentExtras() { - executeWithMainFragment( - mainFragment -> { - Bundle extras = intent.getExtras(); - StringBuilder data = new StringBuilder(); - if (!Utils.isNullOrEmpty(extras.getString(Intent.EXTRA_SUBJECT))) { - data.append(extras.getString(Intent.EXTRA_SUBJECT)); - } - if (!Utils.isNullOrEmpty(extras.getString(Intent.EXTRA_TEXT))) { - data.append(AppConstants.NEW_LINE).append(extras.getString(Intent.EXTRA_TEXT)); - } - String fileName = Long.toString(System.currentTimeMillis()); - AppConfig.getInstance() - .runInBackground( - () -> - MakeFileOperation.mktextfile( - data.toString(), mainFragment.getCurrentPath(), fileName)); - return null; - }); - } + final String type = intent.getType(); - public void clearFabActionItems() { - floatingActionButton.removeActionItemById(R.id.menu_new_folder); - floatingActionButton.removeActionItemById(R.id.menu_new_file); - floatingActionButton.removeActionItemById(R.id.menu_new_cloud); - } + if (actionIntent.equals(Intent.ACTION_GET_CONTENT)) { + // file picker intent + mReturnIntent = true; + Toast.makeText(this, getString(R.string.pick_a_file), Toast.LENGTH_LONG).show(); - /** - * Initializes an interactive shell, which will stay throughout the app lifecycle. - */ - private void initializeInteractiveShell() { - if (isRootExplorer()) { - // Enable mount-master flag when invoking su command, to force su run in the global mount - // namespace. See https://github.com/topjohnwu/libsu/issues/75 - Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)); - Shell.getShell(); - } - } + // disable screen rotation just for convenience purpose + // TODO: Support screen rotation when picking file + Utils.disableScreenRotation(this); + } else if (actionIntent.equals(RingtoneManager.ACTION_RINGTONE_PICKER)) { + // ringtone picker intent + mReturnIntent = true; + mRingtonePickerIntent = true; + Toast.makeText(this, getString(R.string.pick_a_file), Toast.LENGTH_LONG).show(); - /** - * @return paths to all available volumes in the system (include emulated) - */ - public synchronized ArrayList getStorageDirectories() { - ArrayList volumes; - if (SDK_INT >= N) { - volumes = getStorageDirectoriesNew(); - } else { - volumes = getStorageDirectoriesLegacy(); - } - if (isRootExplorer()) { - volumes.add( - new StorageDirectoryParcelable( - "/", - getResources().getString(R.string.root_directory), - R.drawable.ic_drawer_root_white)); - } - return volumes; - } + // disable screen rotation just for convenience purpose + // TODO: Support screen rotation when picking file + Utils.disableScreenRotation(this); + } else if (actionIntent.equals(Intent.ACTION_VIEW)) { + // zip viewer intent + Uri uri = intent.getData(); - /** - * @return All available storage volumes (including internal storage, SD-Cards and USB devices) - */ - @TargetApi(N) - public synchronized ArrayList getStorageDirectoriesNew() { - // Final set of paths - ArrayList volumes = new ArrayList<>(); - StorageManager sm = getSystemService(StorageManager.class); - for (StorageVolume volume : sm.getStorageVolumes()) { - if (!volume.getState().equalsIgnoreCase(Environment.MEDIA_MOUNTED) - && !volume.getState().equalsIgnoreCase(Environment.MEDIA_MOUNTED_READ_ONLY)) { - continue; - } - File path = Utils.getVolumeDirectory(volume); - String name = volume.getDescription(this); - if (INTERNAL_SHARED_STORAGE.equalsIgnoreCase(name)) { - name = getString(R.string.storage_internal); - } - int icon; - if (!volume.isRemovable()) { - icon = R.drawable.ic_phone_android_white_24dp; - } else { - // HACK: There is no reliable way to distinguish USB and SD external storage - // However it is often enough to check for "USB" String - if (name.toUpperCase().contains("USB") || path.getPath().toUpperCase().contains("USB")) { - icon = R.drawable.ic_usb_white_24dp; - } else { - icon = R.drawable.ic_sd_storage_white_24dp; - } - } - volumes.add(new StorageDirectoryParcelable(path.getPath(), name, icon)); - } - return volumes; - } + if (type != null + && (type.equals(ARGS_INTENT_ACTION_VIEW_MIME_FOLDER) + || type.equals(ARGS_INTENT_ACTION_VIEW_APPLICATION_ALL))) { + // support for syncting or intents from external apps that + // need to start file manager from a specific path - /** - * Returns all available SD-Cards in the system (include emulated) - * - *

Warning: Hack! Based on Android source code of version 4.3 (API 18) Because there was no - * standard way to get it before android N - * - * @return All available SD-Cards in the system (include emulated) - */ - public synchronized ArrayList getStorageDirectoriesLegacy() { - List rv = new ArrayList<>(); - - // Primary physical SD-CARD (not emulated) - final String rawExternalStorage = System.getenv("EXTERNAL_STORAGE"); - // All Secondary SD-CARDs (all exclude primary) separated by ":" - final String rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE"); - // Primary emulated SD-CARD - final String rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET"); - if (TextUtils.isEmpty(rawEmulatedStorageTarget)) { - // Device has physical external storage; use plain paths. - if (TextUtils.isEmpty(rawExternalStorage)) { - // EXTERNAL_STORAGE undefined; falling back to default. - // Check for actual existence of the directory before adding to list - if (new File(DEFAULT_FALLBACK_STORAGE_PATH).exists()) { - rv.add(DEFAULT_FALLBACK_STORAGE_PATH); - } else { - // We know nothing else, use Environment's fallback - rv.add(Environment.getExternalStorageDirectory().getAbsolutePath()); - } - } else { - rv.add(rawExternalStorage); - } + if (uri != null) { + + path = Utils.sanitizeInput(FileUtils.fromContentUri(uri).getAbsolutePath()); + scrollToFileName = intent.getStringExtra("com.amaze.fileutilities.AFM_LOCATE_FILE_NAME"); } else { - // Device has emulated storage; external storage paths should have - // userId burned into them. - final String rawUserId; - if (SDK_INT < JELLY_BEAN_MR1) { - rawUserId = ""; - } else { - final String path = Environment.getExternalStorageDirectory().getAbsolutePath(); - final String[] folders = DIR_SEPARATOR.split(path); - final String lastFolder = folders[folders.length - 1]; - boolean isDigit = false; - try { - Integer.valueOf(lastFolder); - isDigit = true; - } catch (NumberFormatException ignored) { - } - rawUserId = isDigit ? lastFolder : ""; - } - // /storage/emulated/0[1,2,...] - if (TextUtils.isEmpty(rawUserId)) { - rv.add(rawEmulatedStorageTarget); + // no data field, open home for the tab in later processing + path = null; + } + } else if (FileUtils.isCompressedFile(Utils.sanitizeInput(uri.toString()))) { + // we don't have folder resource mime type set, supposed to be zip/rar + isCompressedOpen = true; + pathInCompressedArchive = Utils.sanitizeInput(uri.toString()); + openCompressed(pathInCompressedArchive); + } else if (uri.getPath().startsWith("/open_file")) { + /** + * Deeplink to open files directly through amaze using following format: + * http://teamamaze.xyz/open_file?path=path-to-file + */ + path = Utils.sanitizeInput(uri.getQueryParameter("path")); + } else { + LOG.warn(getString(R.string.error_cannot_find_way_open)); + } + + } else if (actionIntent.equals(Intent.ACTION_SEND)) { + if ("text/plain".equals(type)) { + initFabToSave(null); + } else { + // save a single file to filesystem + Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + ArrayList uris = new ArrayList<>(); + uris.add(uri); + initFabToSave(uris); + } + // disable screen rotation just for convenience purpose + // TODO: Support screen rotation when saving a file + Utils.disableScreenRotation(this); + + } else if (actionIntent.equals(Intent.ACTION_SEND_MULTIPLE) && type != null) { + // save multiple files to filesystem + + ArrayList arrayList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + initFabToSave(arrayList); + + // disable screen rotation just for convenience purpose + // TODO: Support screen rotation when saving a file + Utils.disableScreenRotation(this); + } + } + + /** Initializes the floating action button to act as to save data from an external intent */ + private void initFabToSave(final ArrayList uris) { + Utils.showThemedSnackbar( + this, + getString(R.string.select_save_location), + BaseTransientBottomBar.LENGTH_INDEFINITE, + R.string.save, + () -> saveExternalIntent(uris)); + } + + private void saveExternalIntent(final ArrayList uris) { + executeWithMainFragment( + mainFragment -> { + if (uris != null && uris.size() > 0) { + if (SDK_INT >= LOLLIPOP) { + File folder = new File(mainFragment.getCurrentPath()); + int result = mainActivityHelper.checkFolder(folder, MainActivity.this); + if (result == WRITABLE_OR_ON_SDCARD) { + FileUtil.writeUriToStorage( + MainActivity.this, uris, getContentResolver(), mainFragment.getCurrentPath()); + finish(); + } else { + // Trigger SAF intent, keep uri until finish + operation = SAVE_FILE; + urisToBeSaved = uris; + mainActivityHelper.checkFolder(folder, MainActivity.this); + } } else { - rv.add(rawEmulatedStorageTarget + File.separator + rawUserId); + FileUtil.writeUriToStorage( + MainActivity.this, uris, getContentResolver(), mainFragment.getCurrentPath()); } + } else { + saveExternalIntentExtras(); + } + Toast.makeText( + MainActivity.this, + getResources().getString(R.string.saving) + + " to " + + mainFragment.getCurrentPath(), + Toast.LENGTH_LONG) + .show(); + finish(); + return null; + }); + } + + private void saveExternalIntentExtras() { + executeWithMainFragment( + mainFragment -> { + Bundle extras = intent.getExtras(); + StringBuilder data = new StringBuilder(); + if (!Utils.isNullOrEmpty(extras.getString(Intent.EXTRA_SUBJECT))) { + data.append(extras.getString(Intent.EXTRA_SUBJECT)); + } + if (!Utils.isNullOrEmpty(extras.getString(Intent.EXTRA_TEXT))) { + data.append(AppConstants.NEW_LINE).append(extras.getString(Intent.EXTRA_TEXT)); + } + String fileName = Long.toString(System.currentTimeMillis()); + AppConfig.getInstance() + .runInBackground( + () -> + MakeFileOperation.mktextfile( + data.toString(), mainFragment.getCurrentPath(), fileName)); + return null; + }); + } + + public void clearFabActionItems() { + floatingActionButton.removeActionItemById(R.id.menu_new_folder); + floatingActionButton.removeActionItemById(R.id.menu_new_file); + floatingActionButton.removeActionItemById(R.id.menu_new_cloud); + } + + /** Initializes an interactive shell, which will stay throughout the app lifecycle. */ + private void initializeInteractiveShell() { + if (isRootExplorer()) { + // Enable mount-master flag when invoking su command, to force su run in the global mount + // namespace. See https://github.com/topjohnwu/libsu/issues/75 + Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)); + Shell.getShell(); + } + } + + /** + * @return paths to all available volumes in the system (include emulated) + */ + public synchronized ArrayList getStorageDirectories() { + ArrayList volumes; + if (SDK_INT >= N) { + volumes = getStorageDirectoriesNew(); + } else { + volumes = getStorageDirectoriesLegacy(); + } + if (isRootExplorer()) { + volumes.add( + new StorageDirectoryParcelable( + "/", + getResources().getString(R.string.root_directory), + R.drawable.ic_drawer_root_white)); + } + return volumes; + } + + /** + * @return All available storage volumes (including internal storage, SD-Cards and USB devices) + */ + @TargetApi(N) + public synchronized ArrayList getStorageDirectoriesNew() { + // Final set of paths + ArrayList volumes = new ArrayList<>(); + StorageManager sm = getSystemService(StorageManager.class); + for (StorageVolume volume : sm.getStorageVolumes()) { + if (!volume.getState().equalsIgnoreCase(Environment.MEDIA_MOUNTED) + && !volume.getState().equalsIgnoreCase(Environment.MEDIA_MOUNTED_READ_ONLY)) { + continue; + } + File path = Utils.getVolumeDirectory(volume); + String name = volume.getDescription(this); + if (INTERNAL_SHARED_STORAGE.equalsIgnoreCase(name)) { + name = getString(R.string.storage_internal); + } + int icon; + if (!volume.isRemovable()) { + icon = R.drawable.ic_phone_android_white_24dp; + } else { + // HACK: There is no reliable way to distinguish USB and SD external storage + // However it is often enough to check for "USB" String + if (name.toUpperCase().contains("USB") || path.getPath().toUpperCase().contains("USB")) { + icon = R.drawable.ic_usb_white_24dp; + } else { + icon = R.drawable.ic_sd_storage_white_24dp; } - // Add all secondary storages - if (!TextUtils.isEmpty(rawSecondaryStoragesStr)) { - // All Secondary SD-CARDs splited into array - final String[] rawSecondaryStorages = rawSecondaryStoragesStr.split(File.pathSeparator); - Collections.addAll(rv, rawSecondaryStorages); - } - if (SDK_INT >= M && checkStoragePermission()) rv.clear(); - if (SDK_INT >= KITKAT) { - String strings[] = ExternalSdCardOperation.getExtSdCardPathsForActivity(this); - for (String s : strings) { - File f = new File(s); - if (!rv.contains(s) && FileUtils.canListFiles(f)) rv.add(s); - } + } + volumes.add(new StorageDirectoryParcelable(path.getPath(), name, icon)); + } + return volumes; + } + + /** + * Returns all available SD-Cards in the system (include emulated) + * + *

Warning: Hack! Based on Android source code of version 4.3 (API 18) Because there was no + * standard way to get it before android N + * + * @return All available SD-Cards in the system (include emulated) + */ + public synchronized ArrayList getStorageDirectoriesLegacy() { + List rv = new ArrayList<>(); + + // Primary physical SD-CARD (not emulated) + final String rawExternalStorage = System.getenv("EXTERNAL_STORAGE"); + // All Secondary SD-CARDs (all exclude primary) separated by ":" + final String rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE"); + // Primary emulated SD-CARD + final String rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET"); + if (TextUtils.isEmpty(rawEmulatedStorageTarget)) { + // Device has physical external storage; use plain paths. + if (TextUtils.isEmpty(rawExternalStorage)) { + // EXTERNAL_STORAGE undefined; falling back to default. + // Check for actual existence of the directory before adding to list + if (new File(DEFAULT_FALLBACK_STORAGE_PATH).exists()) { + rv.add(DEFAULT_FALLBACK_STORAGE_PATH); + } else { + // We know nothing else, use Environment's fallback + rv.add(Environment.getExternalStorageDirectory().getAbsolutePath()); } - File usb = getUsbDrive(); - if (usb != null && !rv.contains(usb.getPath())) rv.add(usb.getPath()); - - if (SDK_INT >= KITKAT) { - if (SingletonUsbOtg.getInstance().isDeviceConnected()) { - rv.add(OTGUtil.PREFIX_OTG + "/"); - } + } else { + rv.add(rawExternalStorage); + } + } else { + // Device has emulated storage; external storage paths should have + // userId burned into them. + final String rawUserId; + if (SDK_INT < JELLY_BEAN_MR1) { + rawUserId = ""; + } else { + final String path = Environment.getExternalStorageDirectory().getAbsolutePath(); + final String[] folders = DIR_SEPARATOR.split(path); + final String lastFolder = folders[folders.length - 1]; + boolean isDigit = false; + try { + Integer.valueOf(lastFolder); + isDigit = true; + } catch (NumberFormatException ignored) { } + rawUserId = isDigit ? lastFolder : ""; + } + // /storage/emulated/0[1,2,...] + if (TextUtils.isEmpty(rawUserId)) { + rv.add(rawEmulatedStorageTarget); + } else { + rv.add(rawEmulatedStorageTarget + File.separator + rawUserId); + } + } + // Add all secondary storages + if (!TextUtils.isEmpty(rawSecondaryStoragesStr)) { + // All Secondary SD-CARDs splited into array + final String[] rawSecondaryStorages = rawSecondaryStoragesStr.split(File.pathSeparator); + Collections.addAll(rv, rawSecondaryStorages); + } + if (SDK_INT >= M && checkStoragePermission()) rv.clear(); + if (SDK_INT >= KITKAT) { + String strings[] = ExternalSdCardOperation.getExtSdCardPathsForActivity(this); + for (String s : strings) { + File f = new File(s); + if (!rv.contains(s) && FileUtils.canListFiles(f)) rv.add(s); + } + } + File usb = getUsbDrive(); + if (usb != null && !rv.contains(usb.getPath())) rv.add(usb.getPath()); - // Assign a label and icon to each directory - ArrayList volumes = new ArrayList<>(); - for (String file : rv) { - File f = new File(file); - @DrawableRes int icon; - - if ("/storage/emulated/legacy".equals(file) - || "/storage/emulated/0".equals(file) - || "/mnt/sdcard".equals(file)) { - icon = R.drawable.ic_phone_android_white_24dp; - } else if ("/storage/sdcard1".equals(file)) { - icon = R.drawable.ic_sd_storage_white_24dp; - } else if ("/".equals(file)) { - icon = R.drawable.ic_drawer_root_white; - } else { - icon = R.drawable.ic_sd_storage_white_24dp; - } + if (SDK_INT >= KITKAT) { + if (SingletonUsbOtg.getInstance().isDeviceConnected()) { + rv.add(OTGUtil.PREFIX_OTG + "/"); + } + } - @StorageNaming.DeviceDescription - int deviceDescription = StorageNaming.getDeviceDescriptionLegacy(f); - String name = StorageNamingHelper.getNameForDeviceDescription(this, f, deviceDescription); + // Assign a label and icon to each directory + ArrayList volumes = new ArrayList<>(); + for (String file : rv) { + File f = new File(file); + @DrawableRes int icon; + + if ("/storage/emulated/legacy".equals(file) + || "/storage/emulated/0".equals(file) + || "/mnt/sdcard".equals(file)) { + icon = R.drawable.ic_phone_android_white_24dp; + } else if ("/storage/sdcard1".equals(file)) { + icon = R.drawable.ic_sd_storage_white_24dp; + } else if ("/".equals(file)) { + icon = R.drawable.ic_drawer_root_white; + } else { + icon = R.drawable.ic_sd_storage_white_24dp; + } - volumes.add(new StorageDirectoryParcelable(file, name, icon)); - } + @StorageNaming.DeviceDescription + int deviceDescription = StorageNaming.getDeviceDescriptionLegacy(f); + String name = StorageNamingHelper.getNameForDeviceDescription(this, f, deviceDescription); - return volumes; + volumes.add(new StorageDirectoryParcelable(file, name, icon)); } - @Override - public void onBackPressed() { - if (!drawer.isLocked() && drawer.isOpen()) { - drawer.close(); - return; - } - - Fragment fragment = getFragmentAtFrame(); - if (getAppbar().getSearchView().isShown()) { - // hide search view if visible, with an animation - getAppbar().getSearchView().hideSearchView(); - } else if (fragment instanceof TabFragment) { - if (floatingActionButton.isOpen()) { - floatingActionButton.close(true); - } else { - executeWithMainFragment( - mainFragment -> { - mainFragment.goBack(); - return null; - }); - } - } else if (fragment instanceof CompressedExplorerFragment) { - CompressedExplorerFragment compressedExplorerFragment = - (CompressedExplorerFragment) getFragmentAtFrame(); - if (compressedExplorerFragment.mActionMode == null) { - if (compressedExplorerFragment.canGoBack()) { - compressedExplorerFragment.goBack(); - } else if (isCompressedOpen) { - isCompressedOpen = false; - finish(); - } else { - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - fragmentTransaction.setCustomAnimations(R.anim.slide_out_bottom, R.anim.slide_out_bottom); - fragmentTransaction.remove(compressedExplorerFragment); - fragmentTransaction.commit(); - supportInvalidateOptionsMenu(); - floatingActionButton.show(); - } - } else { - compressedExplorerFragment.mActionMode.finish(); - } - } else if (fragment instanceof FtpServerFragment) { - // returning back from FTP server - if (path != null && path.length() > 0) { - HybridFile file = new HybridFile(OpenMode.UNKNOWN, path); - file.generateMode(this); - if (file.isDirectory(this)) goToMain(path); - else { - goToMain(null); - FileUtils.openFile(new File(path), this, getPrefs()); - } - } else { - goToMain(null); - } - } else { - goToMain(null); - } - } + return volumes; + } - public void invalidatePasteSnackbar(boolean showSnackbar) { - if (pasteHelper != null) { - pasteHelper.invalidateSnackbar(this, showSnackbar); - } + @Override + public void onBackPressed() { + if (!drawer.isLocked() && drawer.isOpen()) { + drawer.close(); + return; } - public void exit() { - if (backPressedToExitOnce) { - NetCopyClientConnectionPool.INSTANCE.shutdown(); - finish(); - if (isRootExplorer()) { - closeInteractiveShell(); - } + Fragment fragment = getFragmentAtFrame(); + if (getAppbar().getSearchView().isShown()) { + // hide search view if visible, with an animation + getAppbar().getSearchView().hideSearchView(); + } else if (fragment instanceof TabFragment) { + if (floatingActionButton.isOpen()) { + floatingActionButton.close(true); + } else { + executeWithMainFragment( + mainFragment -> { + mainFragment.goBack(); + return null; + }); + } + } else if (fragment instanceof CompressedExplorerFragment) { + CompressedExplorerFragment compressedExplorerFragment = + (CompressedExplorerFragment) getFragmentAtFrame(); + if (compressedExplorerFragment.mActionMode == null) { + if (compressedExplorerFragment.canGoBack()) { + compressedExplorerFragment.goBack(); + } else if (isCompressedOpen) { + isCompressedOpen = false; + finish(); } else { - this.backPressedToExitOnce = true; - final Toast toast = Toast.makeText(this, getString(R.string.press_again), Toast.LENGTH_SHORT); - this.toast = new WeakReference<>(toast); - toast.show(); - new Handler() - .postDelayed( - () -> { - backPressedToExitOnce = false; - }, - 2000); - } + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.setCustomAnimations(R.anim.slide_out_bottom, R.anim.slide_out_bottom); + fragmentTransaction.remove(compressedExplorerFragment); + fragmentTransaction.commit(); + supportInvalidateOptionsMenu(); + floatingActionButton.show(); + } + } else { + compressedExplorerFragment.mActionMode.finish(); + } + } else if (fragment instanceof FtpServerFragment) { + // returning back from FTP server + if (path != null && path.length() > 0) { + HybridFile file = new HybridFile(OpenMode.UNKNOWN, path); + file.generateMode(this); + if (file.isDirectory(this)) goToMain(path); + else { + goToMain(null); + FileUtils.openFile(new File(path), this, getPrefs()); + } + } else { + goToMain(null); + } + } else { + goToMain(null); } + } - public void goToMain(String path) { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - // title.setText(R.string.app_name); - TabFragment tabFragment = new TabFragment(); - if (intent != null && intent.getAction() != null) { - if (INTENT_ACTION_OPEN_QUICK_ACCESS.equals(intent.getAction())) { - path = "5"; - } else if (INTENT_ACTION_OPEN_RECENT.equals(intent.getAction())) { - path = "6"; - } - } - if (path != null && path.length() > 0) { - Bundle b = new Bundle(); - b.putString("path", path); - tabFragment.setArguments(b); - } - transaction.replace(R.id.content_frame, tabFragment); - // Commit the transaction - transaction.addToBackStack("tabt" + 1); - transaction.commitAllowingStateLoss(); - appbar.setTitle(null); - floatingActionButton.show(); - if (isCompressedOpen && pathInCompressedArchive != null) { - openCompressed(pathInCompressedArchive); - pathInCompressedArchive = null; - } + public void invalidatePasteSnackbar(boolean showSnackbar) { + if (pasteHelper != null) { + pasteHelper.invalidateSnackbar(this, showSnackbar); } + } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater menuInflater = getMenuInflater(); - menuInflater.inflate(R.menu.activity_extra, menu); + public void exit() { + if (backPressedToExitOnce) { + NetCopyClientConnectionPool.INSTANCE.shutdown(); + finish(); + if (isRootExplorer()) { + closeInteractiveShell(); + } + } else { + this.backPressedToExitOnce = true; + final Toast toast = Toast.makeText(this, getString(R.string.press_again), Toast.LENGTH_SHORT); + this.toast = new WeakReference<>(toast); + toast.show(); + new Handler() + .postDelayed( + () -> { + backPressedToExitOnce = false; + }, + 2000); + } + } + + public void goToMain(String path) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + // title.setText(R.string.app_name); + TabFragment tabFragment = new TabFragment(); + if (intent != null && intent.getAction() != null) { + if (INTENT_ACTION_OPEN_QUICK_ACCESS.equals(intent.getAction())) { + path = "5"; + } else if (INTENT_ACTION_OPEN_RECENT.equals(intent.getAction())) { + path = "6"; + } + } + if (path != null && path.length() > 0) { + Bundle b = new Bundle(); + b.putString("path", path); + tabFragment.setArguments(b); + } + transaction.replace(R.id.content_frame, tabFragment); + // Commit the transaction + transaction.addToBackStack("tabt" + 1); + transaction.commitAllowingStateLoss(); + appbar.setTitle(null); + floatingActionButton.show(); + if (isCompressedOpen && pathInCompressedArchive != null) { + openCompressed(pathInCompressedArchive); + pathInCompressedArchive = null; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.activity_extra, menu); /* SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); SearchView searchView = (SearchView) menu.findItem(R.id.search).getActionView(); @@ -1044,236 +1018,236 @@ public boolean onMenuItemActionCollapse(MenuItem item) { } }); */ - return super.onCreateOptionsMenu(menu); - } + return super.onCreateOptionsMenu(menu); + } - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem s = menu.findItem(R.id.view); - MenuItem search = menu.findItem(R.id.search); - Fragment fragment = getFragmentAtFrame(); - if (fragment instanceof TabFragment) { - appbar.setTitle(R.string.appbar_name); - if (getBoolean(PREFERENCE_VIEW)) { - s.setTitle(getResources().getString(R.string.gridview)); - } else { - s.setTitle(getResources().getString(R.string.listview)); - } - try { - executeWithMainFragment( - mainFragment -> { - if (mainFragment.getMainFragmentViewModel().isList()) { - s.setTitle(R.string.gridview); - } else { - s.setTitle(R.string.listview); - } - appbar - .getBottomBar() - .updatePath( - mainFragment.getCurrentPath(), - mainFragment.getMainFragmentViewModel().getResults(), - MainActivityHelper.SEARCH_TEXT, - mainFragment.getMainFragmentViewModel().getOpenMode(), - mainFragment.getMainFragmentViewModel().getFolderCount(), - mainFragment.getMainFragmentViewModel().getFileCount(), - mainFragment); - return null; - }); - } catch (Exception e) { - LOG.warn("failure while preparing options menu", e); - } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem s = menu.findItem(R.id.view); + MenuItem search = menu.findItem(R.id.search); + Fragment fragment = getFragmentAtFrame(); + if (fragment instanceof TabFragment) { + appbar.setTitle(R.string.appbar_name); + if (getBoolean(PREFERENCE_VIEW)) { + s.setTitle(getResources().getString(R.string.gridview)); + } else { + s.setTitle(getResources().getString(R.string.listview)); + } + try { + executeWithMainFragment( + mainFragment -> { + if (mainFragment.getMainFragmentViewModel().isList()) { + s.setTitle(R.string.gridview); + } else { + s.setTitle(R.string.listview); + } + appbar + .getBottomBar() + .updatePath( + mainFragment.getCurrentPath(), + mainFragment.getMainFragmentViewModel().getResults(), + MainActivityHelper.SEARCH_TEXT, + mainFragment.getMainFragmentViewModel().getOpenMode(), + mainFragment.getMainFragmentViewModel().getFolderCount(), + mainFragment.getMainFragmentViewModel().getFileCount(), + mainFragment); + return null; + }); + } catch (Exception e) { + LOG.warn("failure while preparing options menu", e); + } - appbar.getBottomBar().setClickListener(); - - search.setVisible(true); - if (indicator_layout != null) indicator_layout.setVisibility(View.VISIBLE); - menu.findItem(R.id.search).setVisible(true); - menu.findItem(R.id.home).setVisible(true); - menu.findItem(R.id.history).setVisible(true); - menu.findItem(R.id.sethome).setVisible(true); - menu.findItem(R.id.sort).setVisible(true); - menu.findItem(R.id.hiddenitems).setVisible(true); - menu.findItem(R.id.view).setVisible(true); - menu.findItem(R.id.extract).setVisible(false); - invalidatePasteSnackbar(true); - findViewById(R.id.buttonbarframe).setVisibility(View.VISIBLE); - } else if (fragment instanceof AppsListFragment - || fragment instanceof ProcessViewerFragment - || fragment instanceof FtpServerFragment) { - appBarLayout.setExpanded(true); - menu.findItem(R.id.sethome).setVisible(false); - if (indicator_layout != null) indicator_layout.setVisibility(View.GONE); - findViewById(R.id.buttonbarframe).setVisibility(View.GONE); - menu.findItem(R.id.search).setVisible(false); - menu.findItem(R.id.home).setVisible(false); - menu.findItem(R.id.history).setVisible(false); - menu.findItem(R.id.extract).setVisible(false); - if (fragment instanceof ProcessViewerFragment) { - menu.findItem(R.id.sort).setVisible(false); - } else if (fragment instanceof FtpServerFragment) { - menu.findItem(R.id.sort).setVisible(false); - } else { - menu.findItem(R.id.dsort).setVisible(false); - menu.findItem(R.id.sortby).setVisible(false); - } - menu.findItem(R.id.hiddenitems).setVisible(false); - menu.findItem(R.id.view).setVisible(false); - invalidatePasteSnackbar(false); - } else if (fragment instanceof CompressedExplorerFragment) { - appbar.setTitle(R.string.appbar_name); - menu.findItem(R.id.sethome).setVisible(false); - if (indicator_layout != null) indicator_layout.setVisibility(View.GONE); - getAppbar().getBottomBar().resetClickListener(); - menu.findItem(R.id.search).setVisible(false); - menu.findItem(R.id.home).setVisible(false); - menu.findItem(R.id.history).setVisible(false); - menu.findItem(R.id.sort).setVisible(false); - menu.findItem(R.id.hiddenitems).setVisible(false); - menu.findItem(R.id.view).setVisible(false); - menu.findItem(R.id.extract).setVisible(true); - invalidatePasteSnackbar(false); - } - return super.onPrepareOptionsMenu(menu); - } + appbar.getBottomBar().setClickListener(); + + search.setVisible(true); + if (indicator_layout != null) indicator_layout.setVisibility(View.VISIBLE); + menu.findItem(R.id.search).setVisible(true); + menu.findItem(R.id.home).setVisible(true); + menu.findItem(R.id.history).setVisible(true); + menu.findItem(R.id.sethome).setVisible(true); + menu.findItem(R.id.sort).setVisible(true); + menu.findItem(R.id.hiddenitems).setVisible(true); + menu.findItem(R.id.view).setVisible(true); + menu.findItem(R.id.extract).setVisible(false); + invalidatePasteSnackbar(true); + findViewById(R.id.buttonbarframe).setVisibility(View.VISIBLE); + } else if (fragment instanceof AppsListFragment + || fragment instanceof ProcessViewerFragment + || fragment instanceof FtpServerFragment) { + appBarLayout.setExpanded(true); + menu.findItem(R.id.sethome).setVisible(false); + if (indicator_layout != null) indicator_layout.setVisibility(View.GONE); + findViewById(R.id.buttonbarframe).setVisibility(View.GONE); + menu.findItem(R.id.search).setVisible(false); + menu.findItem(R.id.home).setVisible(false); + menu.findItem(R.id.history).setVisible(false); + menu.findItem(R.id.extract).setVisible(false); + if (fragment instanceof ProcessViewerFragment) { + menu.findItem(R.id.sort).setVisible(false); + } else if (fragment instanceof FtpServerFragment) { + menu.findItem(R.id.sort).setVisible(false); + } else { + menu.findItem(R.id.dsort).setVisible(false); + menu.findItem(R.id.sortby).setVisible(false); + } + menu.findItem(R.id.hiddenitems).setVisible(false); + menu.findItem(R.id.view).setVisible(false); + invalidatePasteSnackbar(false); + } else if (fragment instanceof CompressedExplorerFragment) { + appbar.setTitle(R.string.appbar_name); + menu.findItem(R.id.sethome).setVisible(false); + if (indicator_layout != null) indicator_layout.setVisibility(View.GONE); + getAppbar().getBottomBar().resetClickListener(); + menu.findItem(R.id.search).setVisible(false); + menu.findItem(R.id.home).setVisible(false); + menu.findItem(R.id.history).setVisible(false); + menu.findItem(R.id.sort).setVisible(false); + menu.findItem(R.id.hiddenitems).setVisible(false); + menu.findItem(R.id.view).setVisible(false); + menu.findItem(R.id.extract).setVisible(true); + invalidatePasteSnackbar(false); + } + return super.onPrepareOptionsMenu(menu); + } + + // called when the user exits the action mode + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // The action bar home/up action should open or close the drawer. + // ActionBarDrawerToggle will take care of this. + if (drawer.onOptionsItemSelected(item)) return true; + // Same thing goes to other Fragments loaded. + // If they have handled the options, we don't need to. + if (getFragmentAtFrame().onOptionsItemSelected(item)) return true; + + // Handle action buttons + executeWithMainFragment( + mainFragment -> { + switch (item.getItemId()) { + case R.id.home: + mainFragment.home(); + break; + case R.id.history: + HistoryDialog.showHistoryDialog(this, mainFragment); + break; + case R.id.sethome: + if (mainFragment.getMainFragmentViewModel().getOpenMode() != OpenMode.FILE + && mainFragment.getMainFragmentViewModel().getOpenMode() != OpenMode.ROOT) { + Toast.makeText(mainActivity, R.string.not_allowed, Toast.LENGTH_SHORT).show(); + break; + } + final MaterialDialog dialog = + GeneralDialogCreation.showBasicDialog( + mainActivity, + R.string.question_set_path_as_home, + R.string.set_as_home, + R.string.yes, + R.string.no); + dialog + .getActionButton(DialogAction.POSITIVE) + .setOnClickListener( + (v) -> { + mainFragment + .getMainFragmentViewModel() + .setHome(mainFragment.getCurrentPath()); + updatePaths(mainFragment.getMainFragmentViewModel().getNo()); + dialog.dismiss(); + }); + dialog.show(); + break; + case R.id.exit: + finish(); + break; + case R.id.sortby: + GeneralDialogCreation.showSortDialog(mainFragment, getAppTheme(), getPrefs()); + break; + case R.id.dsort: + String[] sort = getResources().getStringArray(R.array.directorysortmode); + MaterialDialog.Builder builder = new MaterialDialog.Builder(mainActivity); + builder.theme(getAppTheme().getMaterialDialogTheme(this)); + builder.title(R.string.directorysort); + int current = + Integer.parseInt( + getPrefs() + .getString(PreferencesConstants.PREFERENCE_DIRECTORY_SORT_MODE, "0")); + + builder + .items(sort) + .itemsCallbackSingleChoice( + current, + (dialog1, view, which, text) -> { + getPrefs() + .edit() + .putString( + PreferencesConstants.PREFERENCE_DIRECTORY_SORT_MODE, "" + which) + .commit(); + mainFragment + .getMainFragmentViewModel() + .initSortModes( + SortHandler.getSortType( + this, mainFragment.getMainFragmentViewModel().getCurrentPath()), + getPrefs()); + mainFragment.updateList(false); + dialog1.dismiss(); + return true; + }); + builder.build().show(); + break; + case R.id.hiddenitems: + HiddenFilesDialog.showHiddenDialog(this, mainFragment); + break; + case R.id.view: + int pathLayout = + dataUtils.getListOrGridForPath(mainFragment.getCurrentPath(), DataUtils.LIST); + if (mainFragment.getMainFragmentViewModel().isList()) { + if (pathLayout == DataUtils.LIST) { + AppConfig.getInstance() + .runInBackground( + () -> { + utilsHandler.removeFromDatabase( + new OperationData( + UtilsHandler.Operation.LIST, mainFragment.getCurrentPath())); + }); + } + utilsHandler.saveToDatabase( + new OperationData(UtilsHandler.Operation.GRID, mainFragment.getCurrentPath())); + + dataUtils.setPathAsGridOrList(mainFragment.getCurrentPath(), DataUtils.GRID); + } else { + if (pathLayout == DataUtils.GRID) { + AppConfig.getInstance() + .runInBackground( + () -> { + utilsHandler.removeFromDatabase( + new OperationData( + UtilsHandler.Operation.GRID, mainFragment.getCurrentPath())); + }); + } - // called when the user exits the action mode - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // The action bar home/up action should open or close the drawer. - // ActionBarDrawerToggle will take care of this. - if (drawer.onOptionsItemSelected(item)) return true; - // Same thing goes to other Fragments loaded. - // If they have handled the options, we don't need to. - if (getFragmentAtFrame().onOptionsItemSelected(item)) return true; - - // Handle action buttons - executeWithMainFragment( - mainFragment -> { - switch (item.getItemId()) { - case R.id.home: - mainFragment.home(); - break; - case R.id.history: - HistoryDialog.showHistoryDialog(this, mainFragment); - break; - case R.id.sethome: - if (mainFragment.getMainFragmentViewModel().getOpenMode() != OpenMode.FILE - && mainFragment.getMainFragmentViewModel().getOpenMode() != OpenMode.ROOT) { - Toast.makeText(mainActivity, R.string.not_allowed, Toast.LENGTH_SHORT).show(); - break; - } - final MaterialDialog dialog = - GeneralDialogCreation.showBasicDialog( - mainActivity, - R.string.question_set_path_as_home, - R.string.set_as_home, - R.string.yes, - R.string.no); - dialog - .getActionButton(DialogAction.POSITIVE) - .setOnClickListener( - (v) -> { - mainFragment - .getMainFragmentViewModel() - .setHome(mainFragment.getCurrentPath()); - updatePaths(mainFragment.getMainFragmentViewModel().getNo()); - dialog.dismiss(); - }); - dialog.show(); - break; - case R.id.exit: - finish(); - break; - case R.id.sortby: - GeneralDialogCreation.showSortDialog(mainFragment, getAppTheme(), getPrefs()); - break; - case R.id.dsort: - String[] sort = getResources().getStringArray(R.array.directorysortmode); - MaterialDialog.Builder builder = new MaterialDialog.Builder(mainActivity); - builder.theme(getAppTheme().getMaterialDialogTheme(this)); - builder.title(R.string.directorysort); - int current = - Integer.parseInt( - getPrefs() - .getString(PreferencesConstants.PREFERENCE_DIRECTORY_SORT_MODE, "0")); - - builder - .items(sort) - .itemsCallbackSingleChoice( - current, - (dialog1, view, which, text) -> { - getPrefs() - .edit() - .putString( - PreferencesConstants.PREFERENCE_DIRECTORY_SORT_MODE, "" + which) - .commit(); - mainFragment - .getMainFragmentViewModel() - .initSortModes( - SortHandler.getSortType( - this, mainFragment.getMainFragmentViewModel().getCurrentPath()), - getPrefs()); - mainFragment.updateList(false); - dialog1.dismiss(); - return true; - }); - builder.build().show(); - break; - case R.id.hiddenitems: - HiddenFilesDialog.showHiddenDialog(this, mainFragment); - break; - case R.id.view: - int pathLayout = - dataUtils.getListOrGridForPath(mainFragment.getCurrentPath(), DataUtils.LIST); - if (mainFragment.getMainFragmentViewModel().isList()) { - if (pathLayout == DataUtils.LIST) { - AppConfig.getInstance() - .runInBackground( - () -> { - utilsHandler.removeFromDatabase( - new OperationData( - UtilsHandler.Operation.LIST, mainFragment.getCurrentPath())); - }); - } - utilsHandler.saveToDatabase( - new OperationData(UtilsHandler.Operation.GRID, mainFragment.getCurrentPath())); - - dataUtils.setPathAsGridOrList(mainFragment.getCurrentPath(), DataUtils.GRID); - } else { - if (pathLayout == DataUtils.GRID) { - AppConfig.getInstance() - .runInBackground( - () -> { - utilsHandler.removeFromDatabase( - new OperationData( - UtilsHandler.Operation.GRID, mainFragment.getCurrentPath())); - }); - } - - utilsHandler.saveToDatabase( - new OperationData(UtilsHandler.Operation.LIST, mainFragment.getCurrentPath())); - - dataUtils.setPathAsGridOrList(mainFragment.getCurrentPath(), DataUtils.LIST); - } - mainFragment.switchView(); - break; - case R.id.extract: - Fragment fragment1 = getFragmentAtFrame(); - if (fragment1 instanceof CompressedExplorerFragment) { - mainActivityHelper.extractFile( - ((CompressedExplorerFragment) fragment1).compressedFile); - } - break; - case R.id.search: - getAppbar().getSearchView().revealSearchView(); - break; - } - return null; - }, - false); - - return super.onOptionsItemSelected(item); - } + utilsHandler.saveToDatabase( + new OperationData(UtilsHandler.Operation.LIST, mainFragment.getCurrentPath())); + + dataUtils.setPathAsGridOrList(mainFragment.getCurrentPath(), DataUtils.LIST); + } + mainFragment.switchView(); + break; + case R.id.extract: + Fragment fragment1 = getFragmentAtFrame(); + if (fragment1 instanceof CompressedExplorerFragment) { + mainActivityHelper.extractFile( + ((CompressedExplorerFragment) fragment1).compressedFile); + } + break; + case R.id.search: + getAppbar().getSearchView().revealSearchView(); + break; + } + return null; + }, + false); + + return super.onOptionsItemSelected(item); + } /*@Override public void onRestoreInstanceState(Bundle savedInstanceState){ @@ -1287,1265 +1261,1255 @@ public void onRestoreInstanceState(Bundle savedInstanceState){ selectedStorage = savedInstanceState.getInt(KEY_DRAWER_SELECTED, 0); }*/ - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - // Sync the toggle state after onRestoreInstanceState has occurred. - drawer.syncState(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - drawer.onConfigurationChanged(newConfig); - // Pass any configuration change to the drawer toggls - } - - @Override - 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); - } - - if (oppathe != null) { - outState.putString(KEY_OPERATION_PATH, oppathe); - outState.putString(KEY_OPERATED_ON_PATH, oppathe1); - outState.putParcelableArrayList(KEY_OPERATIONS_PATH_LIST, (oparrayList)); - outState.putInt(KEY_OPERATION, operation); + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + // Sync the toggle state after onRestoreInstanceState has occurred. + drawer.syncState(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + drawer.onConfigurationChanged(newConfig); + // Pass any configuration change to the drawer toggls + } + + @Override + 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); + } + + if (oppathe != null) { + outState.putString(KEY_OPERATION_PATH, oppathe); + outState.putString(KEY_OPERATED_ON_PATH, oppathe1); + outState.putParcelableArrayList(KEY_OPERATIONS_PATH_LIST, (oparrayList)); + outState.putInt(KEY_OPERATION, operation); + } + } + + @Override + protected void onPause() { + super.onPause(); + unregisterReceiver(mainActivityHelper.mNotificationReceiver); + unregisterReceiver(receiver2); + + if (SDK_INT >= KITKAT) { + unregisterReceiver(mOtgReceiver); + } + + final Toast toast = this.toast.get(); + if (toast != null) { + toast.cancel(); + } + this.toast = new WeakReference<>(null); + } + + @Override + public void onResume() { + super.onResume(); + if (materialDialog != null && !materialDialog.isShowing()) { + materialDialog.show(); + materialDialog = null; + } + + drawer.refreshDrawer(); + drawer.refactorDrawerLockMode(); + + IntentFilter newFilter = new IntentFilter(); + newFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); + newFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + newFilter.addDataScheme(ContentResolver.SCHEME_FILE); + registerReceiver(mainActivityHelper.mNotificationReceiver, newFilter); + registerReceiver(receiver2, new IntentFilter(TAG_INTENT_FILTER_GENERAL)); + + if (SDK_INT >= Build.VERSION_CODES.KITKAT) { + updateUsbInformation(); + } + } + + /** Updates everything related to USB devices MUST ALWAYS be called after onResume() */ + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private void updateUsbInformation() { + boolean isInformationUpdated = false; + List connectedDevices = OTGUtil.getMassStorageDevicesConnected(this); + + if (!connectedDevices.isEmpty()) { + if (SingletonUsbOtg.getInstance().getUsbOtgRoot() != null + && OTGUtil.isUsbUriAccessible(this)) { + for (UsbOtgRepresentation device : connectedDevices) { + if (SingletonUsbOtg.getInstance().checkIfRootIsFromDevice(device)) { + isInformationUpdated = true; + break; + } } - } - @Override - protected void onPause() { - super.onPause(); - unregisterReceiver(mainActivityHelper.mNotificationReceiver); - unregisterReceiver(receiver2); - - if (SDK_INT >= KITKAT) { - unregisterReceiver(mOtgReceiver); + if (!isInformationUpdated) { + SingletonUsbOtg.getInstance().resetUsbOtgRoot(); } + } - final Toast toast = this.toast.get(); - if (toast != null) { - toast.cancel(); - } - this.toast = new WeakReference<>(null); + if (!isInformationUpdated) { + SingletonUsbOtg.getInstance().setConnectedDevice(connectedDevices.get(0)); + isInformationUpdated = true; + } } - @Override - public void onResume() { - super.onResume(); - if (materialDialog != null && !materialDialog.isShowing()) { - materialDialog.show(); - materialDialog = null; - } - - drawer.refreshDrawer(); - drawer.refactorDrawerLockMode(); - - IntentFilter newFilter = new IntentFilter(); - newFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); - newFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); - newFilter.addDataScheme(ContentResolver.SCHEME_FILE); - registerReceiver(mainActivityHelper.mNotificationReceiver, newFilter); - registerReceiver(receiver2, new IntentFilter(TAG_INTENT_FILTER_GENERAL)); - - if (SDK_INT >= Build.VERSION_CODES.KITKAT) { - updateUsbInformation(); - } + if (!isInformationUpdated) { + SingletonUsbOtg.getInstance().resetUsbOtgRoot(); + drawer.refreshDrawer(); } - /** - * Updates everything related to USB devices MUST ALWAYS be called after onResume() - */ - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - private void updateUsbInformation() { - boolean isInformationUpdated = false; - List connectedDevices = OTGUtil.getMassStorageDevicesConnected(this); - - if (!connectedDevices.isEmpty()) { - if (SingletonUsbOtg.getInstance().getUsbOtgRoot() != null - && OTGUtil.isUsbUriAccessible(this)) { - for (UsbOtgRepresentation device : connectedDevices) { - if (SingletonUsbOtg.getInstance().checkIfRootIsFromDevice(device)) { - isInformationUpdated = true; - break; - } - } + // Registering intent filter for OTG + IntentFilter otgFilter = new IntentFilter(); + otgFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + otgFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + registerReceiver(mOtgReceiver, otgFilter); + } - if (!isInformationUpdated) { - SingletonUsbOtg.getInstance().resetUsbOtgRoot(); - } - } - - if (!isInformationUpdated) { - SingletonUsbOtg.getInstance().setConnectedDevice(connectedDevices.get(0)); - isInformationUpdated = true; + /** Receiver to check if a USB device is connected at the runtime of application */ + BroadcastReceiver mOtgReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { + List connectedDevices = + OTGUtil.getMassStorageDevicesConnected(MainActivity.this); + if (!connectedDevices.isEmpty()) { + SingletonUsbOtg.getInstance().resetUsbOtgRoot(); + SingletonUsbOtg.getInstance().setConnectedDevice(connectedDevices.get(0)); + drawer.refreshDrawer(); } - } - - if (!isInformationUpdated) { + } else if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { SingletonUsbOtg.getInstance().resetUsbOtgRoot(); drawer.refreshDrawer(); + goToMain(null); + } } + }; - // Registering intent filter for OTG - IntentFilter otgFilter = new IntentFilter(); - otgFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); - otgFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); - registerReceiver(mOtgReceiver, otgFilter); - } - - /** - * Receiver to check if a USB device is connected at the runtime of application - */ - BroadcastReceiver mOtgReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { - List connectedDevices = - OTGUtil.getMassStorageDevicesConnected(MainActivity.this); - if (!connectedDevices.isEmpty()) { - SingletonUsbOtg.getInstance().resetUsbOtgRoot(); - SingletonUsbOtg.getInstance().setConnectedDevice(connectedDevices.get(0)); - drawer.refreshDrawer(); - } - } else if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { - SingletonUsbOtg.getInstance().resetUsbOtgRoot(); - drawer.refreshDrawer(); - goToMain(null); - } - } - }; - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_MENU) { + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU) { /* ImageView ib = findViewById(R.id.action_overflow); if (ib.getVisibility() == View.VISIBLE) { ib.performClick(); } */ - // return 'true' to prevent further propagation of the key event - return true; - } - - return super.onKeyDown(keyCode, event); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - // TODO: 6/5/2017 Android may choose to not call this method before destruction - // TODO: https://developer.android.com/reference/android/app/Activity.html#onDestroy%28%29 - closeInteractiveShell(); - NetCopyClientConnectionPool.INSTANCE.shutdown(); - if (drawer != null && drawer.getBilling() != null) { - drawer.getBilling().destroyBillingInstance(); - } - } - - /** - * Closes the interactive shell and threads associated - */ - private void closeInteractiveShell() { - if (isRootExplorer()) { - // close interactive shell - try { - Shell.getShell().close(); - } catch (IOException e) { - LOG.error("Error closing Shell", e); - } - } - } - - public void updatePaths(int pos) { - TabFragment tabFragment = getTabFragment(); - if (tabFragment != null) tabFragment.updatePaths(pos); - } - - public void openCompressed(String path) { - appBarLayout.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start(); - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - fragmentTransaction.setCustomAnimations(R.anim.slide_in_top, R.anim.slide_in_bottom); - Fragment zipFragment = new CompressedExplorerFragment(); - Bundle bundle = new Bundle(); - bundle.putString(CompressedExplorerFragment.KEY_PATH, path); - zipFragment.setArguments(bundle); - fragmentTransaction.add(R.id.content_frame, zipFragment); - fragmentTransaction.commitAllowingStateLoss(); - } - - public @Nullable - MainFragment getCurrentMainFragment() { - TabFragment tab = getTabFragment(); - - if (tab != null && tab.getCurrentTabFragment() instanceof MainFragment) { - return (MainFragment) tab.getCurrentTabFragment(); - } else return null; - } - - public TabFragment getTabFragment() { - Fragment fragment = getFragmentAtFrame(); - - if (!(fragment instanceof TabFragment)) return null; - else return (TabFragment) fragment; - } - - public Fragment getFragmentAtFrame() { - return getSupportFragmentManager().findFragmentById(R.id.content_frame); - } - - public void setPagingEnabled(boolean b) { - getTabFragment().setPagingEnabled(b); - } - - public File getUsbDrive() { - File parent = new File("/storage"); - - try { - for (File f : parent.listFiles()) - if (f.exists() && f.getName().toLowerCase().contains("usb") && f.canExecute()) - return f; - } catch (Exception e) { - } - - parent = new File("/mnt/sdcard/usbStorage"); - if (parent.exists() && parent.canExecute()) return parent; - parent = new File("/mnt/sdcard/usb_storage"); - if (parent.exists() && parent.canExecute()) return parent; - - return null; - } - - public SpeedDialView getFAB() { - return floatingActionButton; - } - - public void showFab() { - getFAB().setVisibility(View.VISIBLE); - getFAB().show(); - CoordinatorLayout.LayoutParams params = - (CoordinatorLayout.LayoutParams) getFAB().getLayoutParams(); - params.setBehavior(new SpeedDialView.ScrollingViewSnackbarBehavior()); - getFAB().requestLayout(); - } - - public void hideFab() { - getFAB().setVisibility(View.GONE); - getFAB().hide(); - CoordinatorLayout.LayoutParams params = - (CoordinatorLayout.LayoutParams) getFAB().getLayoutParams(); - params.setBehavior(new SpeedDialView.NoBehavior()); - getFAB().requestLayout(); - } - - public AppBar getAppbar() { - return appbar; - } - - public Drawer getDrawer() { - return drawer; + // return 'true' to prevent further propagation of the key event + return true; + } + + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + // TODO: 6/5/2017 Android may choose to not call this method before destruction + // TODO: https://developer.android.com/reference/android/app/Activity.html#onDestroy%28%29 + closeInteractiveShell(); + NetCopyClientConnectionPool.INSTANCE.shutdown(); + if (drawer != null && drawer.getBilling() != null) { + drawer.getBilling().destroyBillingInstance(); + } + } + + /** Closes the interactive shell and threads associated */ + private void closeInteractiveShell() { + if (isRootExplorer()) { + // close interactive shell + try { + Shell.getShell().close(); + } catch (IOException e) { + LOG.error("Error closing Shell", e); + } } - - protected void onActivityResult(int requestCode, int responseCode, Intent intent) { - super.onActivityResult(requestCode, responseCode, intent); - if (requestCode == Drawer.image_selector_request_code) { - drawer.onActivityResult(requestCode, responseCode, intent); - } else if (requestCode == 3) { - Uri treeUri; - if (responseCode == Activity.RESULT_OK) { - // Get Uri from Storage Access Framework. - treeUri = intent.getData(); - // Persist URI - this is required for verification of writability. - if (treeUri != null) - getPrefs() - .edit() - .putString(PreferencesConstants.PREFERENCE_URI, treeUri.toString()) - .apply(); - } else { - // If not confirmed SAF, or if still not writable, then revert settings. + } + + public void updatePaths(int pos) { + TabFragment tabFragment = getTabFragment(); + if (tabFragment != null) tabFragment.updatePaths(pos); + } + + public void openCompressed(String path) { + appBarLayout.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start(); + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.setCustomAnimations(R.anim.slide_in_top, R.anim.slide_in_bottom); + Fragment zipFragment = new CompressedExplorerFragment(); + Bundle bundle = new Bundle(); + bundle.putString(CompressedExplorerFragment.KEY_PATH, path); + zipFragment.setArguments(bundle); + fragmentTransaction.add(R.id.content_frame, zipFragment); + fragmentTransaction.commitAllowingStateLoss(); + } + + public @Nullable MainFragment getCurrentMainFragment() { + TabFragment tab = getTabFragment(); + + if (tab != null && tab.getCurrentTabFragment() instanceof MainFragment) { + return (MainFragment) tab.getCurrentTabFragment(); + } else return null; + } + + public TabFragment getTabFragment() { + Fragment fragment = getFragmentAtFrame(); + + if (!(fragment instanceof TabFragment)) return null; + else return (TabFragment) fragment; + } + + public Fragment getFragmentAtFrame() { + return getSupportFragmentManager().findFragmentById(R.id.content_frame); + } + + public void setPagingEnabled(boolean b) { + getTabFragment().setPagingEnabled(b); + } + + public File getUsbDrive() { + File parent = new File("/storage"); + + try { + for (File f : parent.listFiles()) + if (f.exists() && f.getName().toLowerCase().contains("usb") && f.canExecute()) return f; + } catch (Exception e) { + } + + parent = new File("/mnt/sdcard/usbStorage"); + if (parent.exists() && parent.canExecute()) return parent; + parent = new File("/mnt/sdcard/usb_storage"); + if (parent.exists() && parent.canExecute()) return parent; + + return null; + } + + public SpeedDialView getFAB() { + return floatingActionButton; + } + + public void showFab() { + getFAB().setVisibility(View.VISIBLE); + getFAB().show(); + CoordinatorLayout.LayoutParams params = + (CoordinatorLayout.LayoutParams) getFAB().getLayoutParams(); + params.setBehavior(new SpeedDialView.ScrollingViewSnackbarBehavior()); + getFAB().requestLayout(); + } + + public void hideFab() { + getFAB().setVisibility(View.GONE); + getFAB().hide(); + CoordinatorLayout.LayoutParams params = + (CoordinatorLayout.LayoutParams) getFAB().getLayoutParams(); + params.setBehavior(new SpeedDialView.NoBehavior()); + getFAB().requestLayout(); + } + + public AppBar getAppbar() { + return appbar; + } + + public Drawer getDrawer() { + return drawer; + } + + protected void onActivityResult(int requestCode, int responseCode, Intent intent) { + super.onActivityResult(requestCode, responseCode, intent); + if (requestCode == Drawer.image_selector_request_code) { + drawer.onActivityResult(requestCode, responseCode, intent); + } else if (requestCode == 3) { + Uri treeUri; + if (responseCode == Activity.RESULT_OK) { + // Get Uri from Storage Access Framework. + treeUri = intent.getData(); + // Persist URI - this is required for verification of writability. + if (treeUri != null) + getPrefs() + .edit() + .putString(PreferencesConstants.PREFERENCE_URI, treeUri.toString()) + .apply(); + } else { + // If not confirmed SAF, or if still not writable, then revert settings. /* DialogUtil.displayError(getActivity(), R.string.message_dialog_cannot_write_to_folder_saf, false, currentFolder); ||!FileUtil.isWritableNormalOrSaf(currentFolder)*/ - return; - } - - // After confirmation, update stored value of folder. - // Persist access permissions. - - if (SDK_INT >= KITKAT) { - getContentResolver() - .takePersistableUriPermission( - treeUri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } - - executeWithMainFragment( - mainFragment -> { - switch (operation) { - case DELETE: // deletion - new DeleteTask(mainActivity).execute((oparrayList)); - break; - case COPY: // copying - // legacy compatibility - if (oparrayList != null && oparrayList.size() != 0) { - oparrayListList = new ArrayList<>(); - oparrayListList.add(oparrayList); - oparrayList = null; - oppatheList = new ArrayList<>(); - oppatheList.add(oppathe); - oppathe = ""; - } - for (int i = 0; i < oparrayListList.size(); i++) { - ArrayList sourceList = oparrayListList.get(i); - Intent intent1 = new Intent(this, CopyService.class); - intent1.putExtra(CopyService.TAG_COPY_SOURCES, sourceList); - intent1.putExtra(CopyService.TAG_COPY_TARGET, oppatheList.get(i)); - ServiceWatcherUtil.runService(this, intent1); - } - break; - case MOVE: // moving - // legacy compatibility - if (oparrayList != null && oparrayList.size() != 0) { - oparrayListList = new ArrayList<>(); - oparrayListList.add(oparrayList); - oparrayList = null; - oppatheList = new ArrayList<>(); - oppatheList.add(oppathe); - oppathe = ""; - } - - TaskKt.fromTask( - new MoveFilesTask( - oparrayListList, - isRootExplorer(), - mainFragment.getCurrentPath(), - this, - OpenMode.FILE, - oppatheList)); - break; - case NEW_FOLDER: // mkdir - mainActivityHelper.mkDir( - new HybridFile(OpenMode.FILE, oppathe), - RootHelper.generateBaseFile(new File(oppathe), true), - mainFragment); - break; - case RENAME: - mainActivityHelper.rename( - mainFragment.getMainFragmentViewModel().getOpenMode(), - (oppathe), - (oppathe1), - null, - false, - mainActivity, - isRootExplorer()); - mainFragment.updateList(false); - break; - case NEW_FILE: - mainActivityHelper.mkFile( - new HybridFile(OpenMode.FILE, oppathe), - new HybridFile(OpenMode.FILE, oppathe), - mainFragment); - break; - case EXTRACT: - mainActivityHelper.extractFile(new File(oppathe)); - break; - case COMPRESS: - mainActivityHelper.compressFiles(new File(oppathe), oparrayList); - break; - case SAVE_FILE: - FileUtil.writeUriToStorage( - this, urisToBeSaved, getContentResolver(), mainFragment.getCurrentPath()); - urisToBeSaved = null; - finish(); - break; - default: - LogHelper.logOnProductionOrCrash("Incorrect value for switch"); - } - return null; - }, - true); - operation = UNDEFINED; - } else if (requestCode == REQUEST_CODE_SAF) { - executeWithMainFragment( - mainFragment -> { - if (responseCode == Activity.RESULT_OK && intent.getData() != null) { - // otg access - Uri usbOtgRoot = intent.getData(); - SingletonUsbOtg.getInstance().setUsbOtgRoot(usbOtgRoot); - mainFragment.loadlist(OTGUtil.PREFIX_OTG, false, OpenMode.OTG, true); - drawer.closeIfNotLocked(); - if (drawer.isLocked()) drawer.onDrawerClosed(); - } else if (requestCode == REQUEST_CODE_SAF_FTP) { - FtpServerFragment ftpServerFragment = (FtpServerFragment) getFragmentAtFrame(); - ftpServerFragment.changeFTPServerPath(intent.getData().toString()); - Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); - - } else { - Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show(); - // otg access not provided - drawer.resetPendingPath(); - } - return null; - }, - true); - } - } - - void initialisePreferences() { - currentTab = getCurrentTab(); - skinStatusBar = PreferenceUtils.getStatusColor(getPrimary()); - } - - void initialiseViews() { - - appbar = - new AppBar( - this, - getPrefs(), - queue -> { - if (!queue.isEmpty()) { - mainActivityHelper.search(getPrefs(), queue); - } - }); - appBarLayout = getAppbar().getAppbarLayout(); - - setSupportActionBar(getAppbar().getToolbar()); - drawer = new Drawer(this); + return; + } - indicator_layout = findViewById(R.id.indicator_layout); + // After confirmation, update stored value of folder. + // Persist access permissions. - getSupportActionBar().setDisplayShowTitleEnabled(false); - fabBgView = findViewById(R.id.fabs_overlay_layout); + if (SDK_INT >= KITKAT) { + getContentResolver() + .takePersistableUriPermission( + treeUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } - switch (getAppTheme().getSimpleTheme(this)) { - case DARK: - fabBgView.setBackgroundResource(R.drawable.fab_shadow_dark); + executeWithMainFragment( + mainFragment -> { + switch (operation) { + case DELETE: // deletion + new DeleteTask(mainActivity).execute((oparrayList)); break; - case BLACK: - fabBgView.setBackgroundResource(R.drawable.fab_shadow_black); + case COPY: // copying + // legacy compatibility + if (oparrayList != null && oparrayList.size() != 0) { + oparrayListList = new ArrayList<>(); + oparrayListList.add(oparrayList); + oparrayList = null; + oppatheList = new ArrayList<>(); + oppatheList.add(oppathe); + oppathe = ""; + } + for (int i = 0; i < oparrayListList.size(); i++) { + ArrayList sourceList = oparrayListList.get(i); + Intent intent1 = new Intent(this, CopyService.class); + intent1.putExtra(CopyService.TAG_COPY_SOURCES, sourceList); + intent1.putExtra(CopyService.TAG_COPY_TARGET, oppatheList.get(i)); + ServiceWatcherUtil.runService(this, intent1); + } break; - } - - fabBgView.setOnClickListener( - view -> { - if (getAppbar().getSearchView().isEnabled()) - getAppbar().getSearchView().hideSearchView(); - }); - - drawer.setDrawerHeaderBackground(); - } - - /** - * Call this method when you need to update the MainActivity view components' colors based on - * update in the {@link MainActivity#currentTab} Warning - All the variables should be initialised - * before calling this method! - */ - public void updateViews(ColorDrawable colorDrawable) { - // appbar view color - appbar.getBottomBar().setBackgroundColor(colorDrawable.getColor()); - // action bar color - mainActivity.getSupportActionBar().setBackgroundDrawable(colorDrawable); - - drawer.setBackgroundColor(colorDrawable.getColor()); - - if (SDK_INT >= LOLLIPOP) { - // for lollipop devices, the status bar color - mainActivity.getWindow().setStatusBarColor(colorDrawable.getColor()); - if (getBoolean(PREFERENCE_COLORED_NAVIGATION)) { - mainActivity - .getWindow() - .setNavigationBarColor(PreferenceUtils.getStatusColor(colorDrawable.getColor())); - } else { - if (getAppTheme().equals(AppTheme.LIGHT)) { - mainActivity - .getWindow() - .setNavigationBarColor(Utils.getColor(this, android.R.color.white)); - } else if (getAppTheme().equals(AppTheme.BLACK)) { - mainActivity - .getWindow() - .setNavigationBarColor(Utils.getColor(this, android.R.color.black)); - } else { - mainActivity - .getWindow() - .setNavigationBarColor(Utils.getColor(this, R.color.holo_dark_background)); + case MOVE: // moving + // legacy compatibility + if (oparrayList != null && oparrayList.size() != 0) { + oparrayListList = new ArrayList<>(); + oparrayListList.add(oparrayList); + oparrayList = null; + oppatheList = new ArrayList<>(); + oppatheList.add(oppathe); + oppathe = ""; } - } - } else if (SDK_INT == KITKAT_WATCH || SDK_INT == KITKAT) { - // for kitkat devices, the status bar color - SystemBarTintManager tintManager = new SystemBarTintManager(this); - tintManager.setStatusBarTintEnabled(true); - tintManager.setStatusBarTintColor(colorDrawable.getColor()); - } - } - - void initialiseFab() { - int colorAccent = getAccent(); - - floatingActionButton = findViewById(R.id.fabs_menu); - floatingActionButton.setMainFabClosedBackgroundColor(colorAccent); - floatingActionButton.setMainFabOpenedBackgroundColor(colorAccent); - initializeFabActionViews(); - } - - public void initializeFabActionViews() { - // NOTE: SpeedDial inverts insert index than FABsmenu - FabWithLabelView cloudFab = - initFabTitle( - R.id.menu_new_cloud, R.string.cloud_connection, R.drawable.ic_cloud_white_24dp); - FabWithLabelView newFileFab = - initFabTitle(R.id.menu_new_file, R.string.file, R.drawable.ic_insert_drive_file_white_48dp); - FabWithLabelView newFolderFab = - initFabTitle(R.id.menu_new_folder, R.string.folder, R.drawable.folder_fab); - - floatingActionButton.setOnActionSelectedListener(new FabActionListener(this)); - floatingActionButton.setOnClickListener( - view -> { - fabButtonClick(cloudFab); - }); - floatingActionButton.setOnFocusChangeListener(new CustomZoomFocusChange()); - floatingActionButton.getMainFab().setOnFocusChangeListener(new CustomZoomFocusChange()); - floatingActionButton.setNextFocusUpId(cloudFab.getId()); - floatingActionButton.getMainFab().setNextFocusUpId(cloudFab.getId()); - floatingActionButton.setOnKeyListener( - (v, keyCode, event) -> { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { - if (getCurrentTab() == 0 && getFAB().isFocused()) { - getTabFragment().setCurrentItem(1); - } - } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { - findViewById(R.id.content_frame).requestFocus(); - } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) { - if (pasteHelper != null - && pasteHelper.getSnackbar() != null - && pasteHelper.getSnackbar().isShown()) - ((Snackbar.SnackbarLayout) pasteHelper.getSnackbar().getView()) - .findViewById(R.id.snackBarActionButton) - .requestFocus(); - } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) { - fabButtonClick(cloudFab); - } else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { - onBackPressed(); - } else { - return false; - } - } - return true; - }); - cloudFab.setNextFocusDownId(floatingActionButton.getMainFab().getId()); - cloudFab.setNextFocusUpId(newFileFab.getId()); - cloudFab.setOnFocusChangeListener(new CustomZoomFocusChange()); - newFileFab.setNextFocusDownId(cloudFab.getId()); - newFileFab.setNextFocusUpId(newFolderFab.getId()); - newFileFab.setOnFocusChangeListener(new CustomZoomFocusChange()); - newFolderFab.setNextFocusDownId(newFileFab.getId()); - newFolderFab.setOnFocusChangeListener(new CustomZoomFocusChange()); - } - - private void fabButtonClick(FabWithLabelView cloudFab) { - if (floatingActionButton.isOpen()) { - floatingActionButton.close(true); - } else { - floatingActionButton.open(true); - cloudFab.requestFocus(); - } - } - - private FabWithLabelView initFabTitle( - @IdRes int id, @StringRes int fabTitle, @DrawableRes int icon) { - int iconSkin = getCurrentColorPreference().getIconSkin(); - - SpeedDialActionItem.Builder builder = - new SpeedDialActionItem.Builder(id, icon) - .setLabel(fabTitle) - .setFabBackgroundColor(iconSkin); - - switch (getAppTheme().getSimpleTheme(this)) { - case LIGHT: - fabBgView.setBackgroundResource(R.drawable.fab_shadow_light); + TaskKt.fromTask( + new MoveFilesTask( + oparrayListList, + isRootExplorer(), + mainFragment.getCurrentPath(), + this, + OpenMode.FILE, + oppatheList)); break; - case DARK: - builder - .setLabelBackgroundColor(Utils.getColor(this, R.color.holo_dark_background)) - .setLabelColor(Utils.getColor(this, R.color.text_dark)); - fabBgView.setBackgroundResource(R.drawable.fab_shadow_dark); + case NEW_FOLDER: // mkdir + mainActivityHelper.mkDir( + new HybridFile(OpenMode.FILE, oppathe), + RootHelper.generateBaseFile(new File(oppathe), true), + mainFragment); break; - case BLACK: - builder - .setLabelBackgroundColor(Color.BLACK) - .setLabelColor(Utils.getColor(this, R.color.text_dark)); - fabBgView.setBackgroundResource(R.drawable.fab_shadow_black); + case RENAME: + mainActivityHelper.rename( + mainFragment.getMainFragmentViewModel().getOpenMode(), + (oppathe), + (oppathe1), + null, + false, + mainActivity, + isRootExplorer()); + mainFragment.updateList(false); break; - } - - return floatingActionButton.addActionItem(builder.create()); - } - - public boolean copyToClipboard(Context context, String text) { - try { - android.content.ClipboardManager clipboard = - (android.content.ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); - android.content.ClipData clip = - android.content.ClipData.newPlainText("Path copied to clipboard", text); - clipboard.setPrimaryClip(clip); - return true; - } catch (Exception e) { - return false; - } - } - - public void renameBookmark(final String title, final String path) { - if (dataUtils.containsBooks(new String[]{title, path}) != -1) { - RenameBookmark renameBookmark = RenameBookmark.getInstance(title, path, getAccent()); - if (renameBookmark != null) renameBookmark.show(getFragmentManager(), "renamedialog"); - } - } - - public PasteHelper getPaste() { - return pasteHelper; - } - - public MainActivityActionMode getActionModeHelper() { - return this.mainActivityActionMode; - } - - public void setPaste(PasteHelper p) { - pasteHelper = p; - } - - @Override - public void onNewIntent(Intent i) { - super.onNewIntent(i); - intent = i; - path = i.getStringExtra("path"); - - if (path != null) { - if (new File(path).isDirectory()) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment != null) { - mainFragment.loadlist(path, false, OpenMode.FILE, true); - } else { - goToMain(path); - } - } else FileUtils.openFile(new File(path), mainActivity, getPrefs()); - } else if (i.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { - ArrayList failedOps = - i.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); - if (failedOps != null) { - mainActivityHelper.showFailedOperationDialog(failedOps, this); - } - } else if (i.getCategories() != null - && i.getCategories().contains(CLOUD_AUTHENTICATOR_GDRIVE)) { - // we used an external authenticator instead of APIs. Probably for Google Drive - CloudRail.setAuthenticationResponse(intent); - if (intent.getAction() != null) { - checkForExternalIntent(intent); - invalidateFragmentAndBundle(null, false); - } - } else if ((openProcesses = i.getBooleanExtra(KEY_INTENT_PROCESS_VIEWER, false))) { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace( - R.id.content_frame, new ProcessViewerFragment(), KEY_INTENT_PROCESS_VIEWER); - // transaction.addToBackStack(null); - openProcesses = false; - // title.setText(utils.getString(con, R.string.process_viewer)); - // Commit the transaction - transaction.commitAllowingStateLoss(); - supportInvalidateOptionsMenu(); - } else if (intent.getAction() != null) { - checkForExternalIntent(intent); - invalidateFragmentAndBundle(null, false); - - if (SDK_INT >= KITKAT) { - if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { - SingletonUsbOtg.getInstance().resetUsbOtgRoot(); - drawer.refreshDrawer(); - } + case NEW_FILE: + mainActivityHelper.mkFile( + new HybridFile(OpenMode.FILE, oppathe), + new HybridFile(OpenMode.FILE, oppathe), + mainFragment); + break; + case EXTRACT: + mainActivityHelper.extractFile(new File(oppathe)); + break; + case COMPRESS: + mainActivityHelper.compressFiles(new File(oppathe), oparrayList); + break; + case SAVE_FILE: + FileUtil.writeUriToStorage( + this, urisToBeSaved, getContentResolver(), mainFragment.getCurrentPath()); + urisToBeSaved = null; + finish(); + break; + default: + LogHelper.logOnProductionOrCrash("Incorrect value for switch"); } - } - } - - private BroadcastReceiver receiver2 = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent i) { - if (i.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { - ArrayList failedOps = - i.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); - if (failedOps != null) { - mainActivityHelper.showFailedOperationDialog(failedOps, mainActivity); - } - } - } - }; - - public void showSMBDialog(String name, String path, boolean edit) { - if (path.length() > 0 && name.length() == 0) { - int i = dataUtils.containsServer(new String[]{name, path}); - if (i != -1) name = dataUtils.getServers().get(i)[0]; - } - SmbConnectDialog smbConnectDialog = new SmbConnectDialog(); - Bundle bundle = new Bundle(); - bundle.putString("name", name); - bundle.putString("path", path); - bundle.putBoolean("edit", edit); - smbConnectDialog.setArguments(bundle); - smbConnectDialog.show(getFragmentManager(), "smbdailog"); - } - - @SuppressLint("CheckResult") - public void showSftpDialog(String name, String path, boolean edit) { - if (path.length() > 0 && name.length() == 0) { - int i = dataUtils.containsServer(new String[]{name, path}); - if (i != -1) name = dataUtils.getServers().get(i)[0]; - } - SftpConnectDialog sftpConnectDialog = new SftpConnectDialog(); - String finalName = name; - Flowable.fromCallable(() -> new NetCopyClientConnectionPool.ConnectionInfo(path)) - .flatMap( - connectionInfo -> { - Bundle retval = new Bundle(); - retval.putString(ARG_PROTOCOL, connectionInfo.getPrefix()); - retval.putString(ARG_NAME, finalName); - retval.putString(ARG_ADDRESS, connectionInfo.getHost()); - retval.putInt(ARG_PORT, connectionInfo.getPort()); - if (!TextUtils.isEmpty(connectionInfo.getDefaultPath())) { - retval.putString(ARG_DEFAULT_PATH, connectionInfo.getDefaultPath()); - } - retval.putString(ARG_USERNAME, connectionInfo.getUsername()); - - if (connectionInfo.getPassword() == null) { - retval.putBoolean(ARG_HAS_PASSWORD, false); - retval.putString(ARG_KEYPAIR_NAME, utilsHandler.getSshAuthPrivateKeyName(path)); - } else { - retval.putBoolean(ARG_HAS_PASSWORD, true); - retval.putString(ARG_PASSWORD, connectionInfo.getPassword()); - } - retval.putBoolean(ARG_EDIT, edit); - return Flowable.just(retval); - }) - .subscribeOn(Schedulers.computation()) - .subscribe( - bundle -> { - sftpConnectDialog.setArguments(bundle); - sftpConnectDialog.setCancelable(true); - sftpConnectDialog.show(getSupportFragmentManager(), "sftpdialog"); - }); - } + return null; + }, + true); + operation = UNDEFINED; + } else if (requestCode == REQUEST_CODE_SAF) { + executeWithMainFragment( + mainFragment -> { + if (responseCode == Activity.RESULT_OK && intent.getData() != null) { + // otg access + Uri usbOtgRoot = intent.getData(); + SingletonUsbOtg.getInstance().setUsbOtgRoot(usbOtgRoot); + mainFragment.loadlist(OTGUtil.PREFIX_OTG, false, OpenMode.OTG, true); + drawer.closeIfNotLocked(); + if (drawer.isLocked()) drawer.onDrawerClosed(); + } else if (requestCode == REQUEST_CODE_SAF_FTP) { + FtpServerFragment ftpServerFragment = (FtpServerFragment) getFragmentAtFrame(); + ftpServerFragment.changeFTPServerPath(intent.getData().toString()); + Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); - /** - * Shows a view that goes from white at it's lowest part to transparent a the top. It covers the - * fragment. - */ - public void showSmokeScreen() { - fabBgView.show(); - } - - public void hideSmokeScreen() { - fabBgView.hide(); - } - - @Override - @SuppressLint("CheckResult") - public void addConnection( - boolean edit, - @NonNull final String name, - @NonNull final String path, - @Nullable final String encryptedPath, - @Nullable final String oldname, - @Nullable final String oldPath) { - String[] s = new String[]{name, path}; - if (!edit) { - if ((dataUtils.containsServer(path)) == -1) { - Completable.fromRunnable( - () -> { - utilsHandler.saveToDatabase( - new OperationData(UtilsHandler.Operation.SMB, name, encryptedPath)); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - () -> { - dataUtils.addServer(s); - drawer.refreshDrawer(); - // grid.addPath(name, encryptedPath, DataUtils.SMB, 1); - executeWithMainFragment( - mainFragment -> { - mainFragment.loadlist(path, false, OpenMode.UNKNOWN, true); - return null; - }, - true); - }); } else { - Snackbar.make( - findViewById(R.id.navigation), - getString(R.string.connection_exists), - Snackbar.LENGTH_SHORT) - .show(); + Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show(); + // otg access not provided + drawer.resetPendingPath(); } + return null; + }, + true); + } + } + + void initialisePreferences() { + currentTab = getCurrentTab(); + skinStatusBar = PreferenceUtils.getStatusColor(getPrimary()); + } + + void initialiseViews() { + + appbar = + new AppBar( + this, + getPrefs(), + queue -> { + if (!queue.isEmpty()) { + mainActivityHelper.search(getPrefs(), queue); + } + }); + appBarLayout = getAppbar().getAppbarLayout(); + + setSupportActionBar(getAppbar().getToolbar()); + drawer = new Drawer(this); + + indicator_layout = findViewById(R.id.indicator_layout); + + getSupportActionBar().setDisplayShowTitleEnabled(false); + fabBgView = findViewById(R.id.fabs_overlay_layout); + + switch (getAppTheme().getSimpleTheme(this)) { + case DARK: + fabBgView.setBackgroundResource(R.drawable.fab_shadow_dark); + break; + case BLACK: + fabBgView.setBackgroundResource(R.drawable.fab_shadow_black); + break; + } + + fabBgView.setOnClickListener( + view -> { + if (getAppbar().getSearchView().isEnabled()) getAppbar().getSearchView().hideSearchView(); + }); + + drawer.setDrawerHeaderBackground(); + } + + /** + * Call this method when you need to update the MainActivity view components' colors based on + * update in the {@link MainActivity#currentTab} Warning - All the variables should be initialised + * before calling this method! + */ + public void updateViews(ColorDrawable colorDrawable) { + // appbar view color + appbar.getBottomBar().setBackgroundColor(colorDrawable.getColor()); + // action bar color + mainActivity.getSupportActionBar().setBackgroundDrawable(colorDrawable); + + drawer.setBackgroundColor(colorDrawable.getColor()); + + if (SDK_INT >= LOLLIPOP) { + // for lollipop devices, the status bar color + mainActivity.getWindow().setStatusBarColor(colorDrawable.getColor()); + if (getBoolean(PREFERENCE_COLORED_NAVIGATION)) { + mainActivity + .getWindow() + .setNavigationBarColor(PreferenceUtils.getStatusColor(colorDrawable.getColor())); + } else { + if (getAppTheme().equals(AppTheme.LIGHT)) { + mainActivity + .getWindow() + .setNavigationBarColor(Utils.getColor(this, android.R.color.white)); + } else if (getAppTheme().equals(AppTheme.BLACK)) { + mainActivity + .getWindow() + .setNavigationBarColor(Utils.getColor(this, android.R.color.black)); } else { - int i = dataUtils.containsServer(new String[]{oldname, oldPath}); - if (i != -1) { - dataUtils.removeServer(i); - - AppConfig.getInstance() - .runInBackground( - () -> { - utilsHandler.renameSMB(oldname, oldPath, name, path); - }); - // mainActivity.grid.removePath(oldname, oldPath, DataUtils.SMB); - } - dataUtils.addServer(s); - Collections.sort(dataUtils.getServers(), new BookSorter()); - drawer.refreshDrawer(); - // mainActivity.grid.addPath(name, encryptedPath, DataUtils.SMB, 1); + mainActivity + .getWindow() + .setNavigationBarColor(Utils.getColor(this, R.color.holo_dark_background)); } - } - - @Override - @SuppressLint("CheckResult") - public void deleteConnection(final String name, final String path) { - int i = dataUtils.containsServer(new String[]{name, path}); - if (i != -1) { - dataUtils.removeServer(i); - Completable.fromCallable( - () -> { - utilsHandler.removeFromDatabase( - new OperationData(UtilsHandler.Operation.SMB, name, path)); - return true; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> drawer.refreshDrawer()); - } - } - - @Override - @SuppressLint("CheckResult") - public void delete(String title, String path) { - Completable.fromCallable( - () -> { - utilsHandler.removeFromDatabase( - new OperationData(UtilsHandler.Operation.BOOKMARKS, title, path)); - return true; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> drawer.refreshDrawer()); - } - - @Override - @SuppressLint("CheckResult") - public void modify(String oldpath, String oldname, String newPath, String newname) { - Completable.fromCallable( - () -> { - utilsHandler.renameBookmark(oldname, oldpath, newname, newPath); - return true; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> drawer.refreshDrawer()); - } - - @Override - public void onPreExecute(String query) { - executeWithMainFragment( - mainFragment -> { - mainFragment.mSwipeRefreshLayout.setRefreshing(true); - mainFragment.onSearchPreExecute(query); - return null; - }); - } - - @Override - public void onPostExecute(String query) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment == null) { - // TODO cancel search - return; - } - - mainFragment.onSearchCompleted(query); - mainFragment.mSwipeRefreshLayout.setRefreshing(false); - } - - @Override - public void onProgressUpdate(@NonNull HybridFileParcelable hybridFileParcelable, String query) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment == null) { - // TODO cancel search - return; - } - - mainFragment.addSearchResult(hybridFileParcelable, query); - } - - @Override - public void onCancelled() { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment == null) { - return; - } - - mainFragment.reloadListElements( - false, false, !mainFragment.getMainFragmentViewModel().isList()); - mainFragment.mSwipeRefreshLayout.setRefreshing(false); - } - - @Override - public void addConnection(OpenMode service) { - try { - if (cloudHandler.findEntry(service) != null) { - // cloud entry already exists - Toast.makeText( - this, getResources().getString(R.string.connection_exists), Toast.LENGTH_LONG) - .show(); - } else if (BuildConfig.IS_VERSION_FDROID) { - Toast.makeText( - this, getResources().getString(R.string.cloud_error_fdroid), Toast.LENGTH_LONG) - .show(); + } + } else if (SDK_INT == KITKAT_WATCH || SDK_INT == KITKAT) { + + // for kitkat devices, the status bar color + SystemBarTintManager tintManager = new SystemBarTintManager(this); + tintManager.setStatusBarTintEnabled(true); + tintManager.setStatusBarTintColor(colorDrawable.getColor()); + } + } + + void initialiseFab() { + int colorAccent = getAccent(); + + floatingActionButton = findViewById(R.id.fabs_menu); + floatingActionButton.setMainFabClosedBackgroundColor(colorAccent); + floatingActionButton.setMainFabOpenedBackgroundColor(colorAccent); + initializeFabActionViews(); + } + + public void initializeFabActionViews() { + // NOTE: SpeedDial inverts insert index than FABsmenu + FabWithLabelView cloudFab = + initFabTitle( + R.id.menu_new_cloud, R.string.cloud_connection, R.drawable.ic_cloud_white_24dp); + FabWithLabelView newFileFab = + initFabTitle(R.id.menu_new_file, R.string.file, R.drawable.ic_insert_drive_file_white_48dp); + FabWithLabelView newFolderFab = + initFabTitle(R.id.menu_new_folder, R.string.folder, R.drawable.folder_fab); + + floatingActionButton.setOnActionSelectedListener(new FabActionListener(this)); + floatingActionButton.setOnClickListener( + view -> { + fabButtonClick(cloudFab); + }); + floatingActionButton.setOnFocusChangeListener(new CustomZoomFocusChange()); + floatingActionButton.getMainFab().setOnFocusChangeListener(new CustomZoomFocusChange()); + floatingActionButton.setNextFocusUpId(cloudFab.getId()); + floatingActionButton.getMainFab().setNextFocusUpId(cloudFab.getId()); + floatingActionButton.setOnKeyListener( + (v, keyCode, event) -> { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { + if (getCurrentTab() == 0 && getFAB().isFocused()) { + getTabFragment().setCurrentItem(1); + } + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { + findViewById(R.id.content_frame).requestFocus(); + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) { + if (pasteHelper != null + && pasteHelper.getSnackbar() != null + && pasteHelper.getSnackbar().isShown()) + ((Snackbar.SnackbarLayout) pasteHelper.getSnackbar().getView()) + .findViewById(R.id.snackBarActionButton) + .requestFocus(); + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) { + fabButtonClick(cloudFab); + } else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + onBackPressed(); } else { - Toast.makeText( - MainActivity.this, - getResources().getString(R.string.please_wait), - Toast.LENGTH_LONG) - .show(); - Bundle args = new Bundle(); - args.putInt(ARGS_KEY_LOADER, service.ordinal()); - - // check if we already had done some work on the loader - Loader loader = getSupportLoaderManager().getLoader(REQUEST_CODE_CLOUD_LIST_KEY); - if (loader != null && loader.isStarted()) { - - // making sure that loader is not started - getSupportLoaderManager().destroyLoader(REQUEST_CODE_CLOUD_LIST_KEY); - } - - getSupportLoaderManager().initLoader(REQUEST_CODE_CLOUD_LIST_KEY, args, this); + return false; } - } catch (CloudPluginException e) { - LOG.warn("failure when adding cloud plugin connections", e); - Toast.makeText(this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) - .show(); - } - } - - @Override - public void deleteConnection(OpenMode service) { - cloudHandler.clear(service); - dataUtils.removeAccount(service); - - runOnUiThread(drawer::refreshDrawer); - } - - @NonNull - @Override - public Loader onCreateLoader(int id, Bundle args) { - Uri uri = - Uri.withAppendedPath( - Uri.parse("content://" + CloudContract.PROVIDER_AUTHORITY), "/keys.db/secret_keys"); - - String[] projection = - new String[]{ - CloudContract.COLUMN_ID, - CloudContract.COLUMN_CLIENT_ID, - CloudContract.COLUMN_CLIENT_SECRET_KEY - }; - - switch (id) { - case REQUEST_CODE_CLOUD_LIST_KEY: - Uri uriAppendedPath = uri; - switch (OpenMode.getOpenMode(args.getInt(ARGS_KEY_LOADER, 2))) { - case GDRIVE: - uriAppendedPath = ContentUris.withAppendedId(uri, 2); - break; - case DROPBOX: - uriAppendedPath = ContentUris.withAppendedId(uri, 3); - break; - case BOX: - uriAppendedPath = ContentUris.withAppendedId(uri, 4); - break; - case ONEDRIVE: - uriAppendedPath = ContentUris.withAppendedId(uri, 5); - break; - } - return new CursorLoader(this, uriAppendedPath, projection, null, null, null); - case REQUEST_CODE_CLOUD_LIST_KEYS: - // we need a list of all secret keys - - try { - List cloudEntries = cloudHandler.getAllEntries(); - - // we want keys for services saved in database, and the cloudrail app key which - // is at index 1 - String ids[] = new String[cloudEntries.size() + 1]; - - ids[0] = 1 + ""; - for (int i = 1; i <= cloudEntries.size(); i++) { - - // we need to get only those cloud details which user wants - switch (cloudEntries.get(i - 1).getServiceType()) { - case GDRIVE: - ids[i] = 2 + ""; - break; - case DROPBOX: - ids[i] = 3 + ""; - break; - case BOX: - ids[i] = 4 + ""; - break; - case ONEDRIVE: - ids[i] = 5 + ""; - break; - } - } - return new CursorLoader(this, uri, projection, CloudContract.COLUMN_ID, ids, null); - } catch (CloudPluginException e) { - LOG.warn("failure when fetching cloud connections", e); - Toast.makeText( - this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) - .show(); - } - default: - Uri undefinedUriAppendedPath = ContentUris.withAppendedId(uri, 7); - return new CursorLoader(this, undefinedUriAppendedPath, projection, null, null, null); - } - } - - @Override - public void onLoadFinished(Loader loader, final Cursor data) { - if (data == null) { - Toast.makeText( - this, - getResources().getString(R.string.cloud_error_failed_restart), - Toast.LENGTH_LONG) - .show(); - return; - } - - /* - * This is hack for repeated calls to onLoadFinished(), - * we take the Cursor provided to check if the function - * has already been called on it. - * - * TODO: find a fix for repeated callbacks to onLoadFinished() - */ - if (cloudCursorData != null && cloudCursorData == data) return; - cloudCursorData = data; - - if (cloudLoaderAsyncTask != null - && cloudLoaderAsyncTask.getStatus() == AsyncTask.Status.RUNNING) { - return; - } - cloudLoaderAsyncTask = new CloudLoaderAsyncTask(this, cloudHandler, cloudCursorData); - cloudLoaderAsyncTask.execute(); - } - - @Override - public void onLoaderReset(Loader loader) { - // For passing code check - } - - public void initCornersDragListener(boolean destroy, boolean shouldInvokeLeftAndRight) { - initBottomDragListener(destroy); - initLeftRightAndTopDragListeners(destroy, shouldInvokeLeftAndRight); - } - - private void initBottomDragListener(boolean destroy) { - View bottomPlaceholder = findViewById(R.id.placeholder_drag_bottom); - if (destroy) { - bottomPlaceholder.setOnDragListener(null); - bottomPlaceholder.setVisibility(View.GONE); + } + return true; + }); + cloudFab.setNextFocusDownId(floatingActionButton.getMainFab().getId()); + cloudFab.setNextFocusUpId(newFileFab.getId()); + cloudFab.setOnFocusChangeListener(new CustomZoomFocusChange()); + newFileFab.setNextFocusDownId(cloudFab.getId()); + newFileFab.setNextFocusUpId(newFolderFab.getId()); + newFileFab.setOnFocusChangeListener(new CustomZoomFocusChange()); + newFolderFab.setNextFocusDownId(newFileFab.getId()); + newFolderFab.setOnFocusChangeListener(new CustomZoomFocusChange()); + } + + private void fabButtonClick(FabWithLabelView cloudFab) { + if (floatingActionButton.isOpen()) { + floatingActionButton.close(true); + } else { + floatingActionButton.open(true); + cloudFab.requestFocus(); + } + } + + private FabWithLabelView initFabTitle( + @IdRes int id, @StringRes int fabTitle, @DrawableRes int icon) { + int iconSkin = getCurrentColorPreference().getIconSkin(); + + SpeedDialActionItem.Builder builder = + new SpeedDialActionItem.Builder(id, icon) + .setLabel(fabTitle) + .setFabBackgroundColor(iconSkin); + + switch (getAppTheme().getSimpleTheme(this)) { + case LIGHT: + fabBgView.setBackgroundResource(R.drawable.fab_shadow_light); + break; + case DARK: + builder + .setLabelBackgroundColor(Utils.getColor(this, R.color.holo_dark_background)) + .setLabelColor(Utils.getColor(this, R.color.text_dark)); + fabBgView.setBackgroundResource(R.drawable.fab_shadow_dark); + break; + case BLACK: + builder + .setLabelBackgroundColor(Color.BLACK) + .setLabelColor(Utils.getColor(this, R.color.text_dark)); + fabBgView.setBackgroundResource(R.drawable.fab_shadow_black); + break; + } + + return floatingActionButton.addActionItem(builder.create()); + } + + public boolean copyToClipboard(Context context, String text) { + try { + android.content.ClipboardManager clipboard = + (android.content.ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); + android.content.ClipData clip = + android.content.ClipData.newPlainText("Path copied to clipboard", text); + clipboard.setPrimaryClip(clip); + return true; + } catch (Exception e) { + return false; + } + } + + public void renameBookmark(final String title, final String path) { + if (dataUtils.containsBooks(new String[] {title, path}) != -1) { + RenameBookmark renameBookmark = RenameBookmark.getInstance(title, path, getAccent()); + if (renameBookmark != null) renameBookmark.show(getFragmentManager(), "renamedialog"); + } + } + + public PasteHelper getPaste() { + return pasteHelper; + } + + public MainActivityActionMode getActionModeHelper() { + return this.mainActivityActionMode; + } + + public void setPaste(PasteHelper p) { + pasteHelper = p; + } + + @Override + public void onNewIntent(Intent i) { + super.onNewIntent(i); + intent = i; + path = i.getStringExtra("path"); + + if (path != null) { + if (new File(path).isDirectory()) { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment != null) { + mainFragment.loadlist(path, false, OpenMode.FILE, true); } else { - bottomPlaceholder.setVisibility(View.VISIBLE); - bottomPlaceholder.setOnDragListener( - new TabFragmentBottomDragListener( - () -> { - getCurrentMainFragment().smoothScrollListView(false); - return null; - }, - () -> { - getCurrentMainFragment().stopSmoothScrollListView(); - return null; - })); + goToMain(path); + } + } else FileUtils.openFile(new File(path), mainActivity, getPrefs()); + } else if (i.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { + ArrayList failedOps = + i.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); + if (failedOps != null) { + mainActivityHelper.showFailedOperationDialog(failedOps, this); + } + } else if (i.getCategories() != null + && i.getCategories().contains(CLOUD_AUTHENTICATOR_GDRIVE)) { + // we used an external authenticator instead of APIs. Probably for Google Drive + CloudRail.setAuthenticationResponse(intent); + if (intent.getAction() != null) { + checkForExternalIntent(intent); + invalidateFragmentAndBundle(null, false); + } + } else if ((openProcesses = i.getBooleanExtra(KEY_INTENT_PROCESS_VIEWER, false))) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace( + R.id.content_frame, new ProcessViewerFragment(), KEY_INTENT_PROCESS_VIEWER); + // transaction.addToBackStack(null); + openProcesses = false; + // title.setText(utils.getString(con, R.string.process_viewer)); + // Commit the transaction + transaction.commitAllowingStateLoss(); + supportInvalidateOptionsMenu(); + } else if (intent.getAction() != null) { + checkForExternalIntent(intent); + invalidateFragmentAndBundle(null, false); + + if (SDK_INT >= KITKAT) { + if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + SingletonUsbOtg.getInstance().resetUsbOtgRoot(); + drawer.refreshDrawer(); } + } } + } - private void initLeftRightAndTopDragListeners(boolean destroy, boolean shouldInvokeLeftAndRight) { - TabFragment tabFragment = getTabFragment(); - tabFragment.initLeftRightAndTopDragListeners(destroy, shouldInvokeLeftAndRight); - } - - private static final class FabActionListener implements SpeedDialView.OnActionSelectedListener { - - MainActivity mainActivity; - SpeedDialView floatingActionButton; - - FabActionListener(MainActivity mainActivity) { - this.mainActivity = mainActivity; - this.floatingActionButton = mainActivity.floatingActionButton; - } - + private BroadcastReceiver receiver2 = + new BroadcastReceiver() { @Override - public boolean onActionSelected(SpeedDialActionItem actionItem) { - final MainFragment ma = - (MainFragment) - ((TabFragment) - mainActivity.getSupportFragmentManager().findFragmentById(R.id.content_frame)) - .getCurrentTabFragment(); - final String path = ma.getCurrentPath(); - - switch (actionItem.getId()) { - case R.id.menu_new_folder: - mainActivity.mainActivityHelper.mkdir( - ma.getMainFragmentViewModel().getOpenMode(), path, ma); - break; - case R.id.menu_new_file: - mainActivity.mainActivityHelper.mkfile( - ma.getMainFragmentViewModel().getOpenMode(), path, ma); - break; - case R.id.menu_new_cloud: - BottomSheetDialogFragment fragment = new CloudSheetFragment(); - fragment.show( - ma.getActivity().getSupportFragmentManager(), CloudSheetFragment.TAG_FRAGMENT); - break; + public void onReceive(Context context, Intent i) { + if (i.getStringArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS) != null) { + ArrayList failedOps = + i.getParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS); + if (failedOps != null) { + mainActivityHelper.showFailedOperationDialog(failedOps, mainActivity); } + } + } + }; + + public void showSMBDialog(String name, String path, boolean edit) { + if (path.length() > 0 && name.length() == 0) { + int i = dataUtils.containsServer(new String[] {name, path}); + if (i != -1) name = dataUtils.getServers().get(i)[0]; + } + SmbConnectDialog smbConnectDialog = new SmbConnectDialog(); + Bundle bundle = new Bundle(); + bundle.putString("name", name); + bundle.putString("path", path); + bundle.putBoolean("edit", edit); + smbConnectDialog.setArguments(bundle); + smbConnectDialog.show(getFragmentManager(), "smbdailog"); + } + + @SuppressLint("CheckResult") + public void showSftpDialog(String name, String path, boolean edit) { + if (path.length() > 0 && name.length() == 0) { + int i = dataUtils.containsServer(new String[] {name, path}); + if (i != -1) name = dataUtils.getServers().get(i)[0]; + } + SftpConnectDialog sftpConnectDialog = new SftpConnectDialog(); + String finalName = name; + Flowable.fromCallable(() -> new NetCopyClientConnectionPool.ConnectionInfo(path)) + .flatMap( + connectionInfo -> { + Bundle retval = new Bundle(); + retval.putString(ARG_PROTOCOL, connectionInfo.getPrefix()); + retval.putString(ARG_NAME, finalName); + retval.putString(ARG_ADDRESS, connectionInfo.getHost()); + retval.putInt(ARG_PORT, connectionInfo.getPort()); + if (!TextUtils.isEmpty(connectionInfo.getDefaultPath())) { + retval.putString(ARG_DEFAULT_PATH, connectionInfo.getDefaultPath()); + } + retval.putString(ARG_USERNAME, connectionInfo.getUsername()); + + if (connectionInfo.getPassword() == null) { + retval.putBoolean(ARG_HAS_PASSWORD, false); + retval.putString(ARG_KEYPAIR_NAME, utilsHandler.getSshAuthPrivateKeyName(path)); + } else { + retval.putBoolean(ARG_HAS_PASSWORD, true); + retval.putString(ARG_PASSWORD, connectionInfo.getPassword()); + } + retval.putBoolean(ARG_EDIT, edit); + return Flowable.just(retval); + }) + .subscribeOn(Schedulers.computation()) + .subscribe( + bundle -> { + sftpConnectDialog.setArguments(bundle); + sftpConnectDialog.setCancelable(true); + sftpConnectDialog.show(getSupportFragmentManager(), "sftpdialog"); + }); + } + + /** + * Shows a view that goes from white at it's lowest part to transparent a the top. It covers the + * fragment. + */ + public void showSmokeScreen() { + fabBgView.show(); + } + + public void hideSmokeScreen() { + fabBgView.hide(); + } + + @Override + @SuppressLint("CheckResult") + public void addConnection( + boolean edit, + @NonNull final String name, + @NonNull final String path, + @Nullable final String encryptedPath, + @Nullable final String oldname, + @Nullable final String oldPath) { + String[] s = new String[] {name, path}; + if (!edit) { + if ((dataUtils.containsServer(path)) == -1) { + Completable.fromRunnable( + () -> { + utilsHandler.saveToDatabase( + new OperationData(UtilsHandler.Operation.SMB, name, encryptedPath)); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> { + dataUtils.addServer(s); + drawer.refreshDrawer(); + // grid.addPath(name, encryptedPath, DataUtils.SMB, 1); + executeWithMainFragment( + mainFragment -> { + mainFragment.loadlist(path, false, OpenMode.UNKNOWN, true); + return null; + }, + true); + }); + } else { + Snackbar.make( + findViewById(R.id.navigation), + getString(R.string.connection_exists), + Snackbar.LENGTH_SHORT) + .show(); + } + } else { + int i = dataUtils.containsServer(new String[] {oldname, oldPath}); + if (i != -1) { + dataUtils.removeServer(i); + + AppConfig.getInstance() + .runInBackground( + () -> { + utilsHandler.renameSMB(oldname, oldPath, name, path); + }); + // mainActivity.grid.removePath(oldname, oldPath, DataUtils.SMB); + } + dataUtils.addServer(s); + Collections.sort(dataUtils.getServers(), new BookSorter()); + drawer.refreshDrawer(); + // mainActivity.grid.addPath(name, encryptedPath, DataUtils.SMB, 1); + } + } + + @Override + @SuppressLint("CheckResult") + public void deleteConnection(final String name, final String path) { + int i = dataUtils.containsServer(new String[] {name, path}); + if (i != -1) { + dataUtils.removeServer(i); + Completable.fromCallable( + () -> { + utilsHandler.removeFromDatabase( + new OperationData(UtilsHandler.Operation.SMB, name, path)); + return true; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> drawer.refreshDrawer()); + } + } + + @Override + @SuppressLint("CheckResult") + public void delete(String title, String path) { + Completable.fromCallable( + () -> { + utilsHandler.removeFromDatabase( + new OperationData(UtilsHandler.Operation.BOOKMARKS, title, path)); + return true; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> drawer.refreshDrawer()); + } + + @Override + @SuppressLint("CheckResult") + public void modify(String oldpath, String oldname, String newPath, String newname) { + Completable.fromCallable( + () -> { + utilsHandler.renameBookmark(oldname, oldpath, newname, newPath); + return true; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> drawer.refreshDrawer()); + } + + @Override + public void onPreExecute(String query) { + executeWithMainFragment( + mainFragment -> { + mainFragment.mSwipeRefreshLayout.setRefreshing(true); + mainFragment.onSearchPreExecute(query); + return null; + }); + } + + @Override + public void onPostExecute(String query) { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment == null) { + // TODO cancel search + return; + } + + mainFragment.onSearchCompleted(query); + mainFragment.mSwipeRefreshLayout.setRefreshing(false); + } + + @Override + public void onProgressUpdate(@NonNull HybridFileParcelable hybridFileParcelable, String query) { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment == null) { + // TODO cancel search + return; + } + + mainFragment.addSearchResult(hybridFileParcelable, query); + } + + @Override + public void onCancelled() { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment == null) { + return; + } + + mainFragment.reloadListElements( + false, false, !mainFragment.getMainFragmentViewModel().isList()); + mainFragment.mSwipeRefreshLayout.setRefreshing(false); + } + + @Override + public void addConnection(OpenMode service) { + try { + if (cloudHandler.findEntry(service) != null) { + // cloud entry already exists + Toast.makeText( + this, getResources().getString(R.string.connection_exists), Toast.LENGTH_LONG) + .show(); + } else if (BuildConfig.IS_VERSION_FDROID) { + Toast.makeText( + this, getResources().getString(R.string.cloud_error_fdroid), Toast.LENGTH_LONG) + .show(); + } else { + Toast.makeText( + MainActivity.this, + getResources().getString(R.string.please_wait), + Toast.LENGTH_LONG) + .show(); + Bundle args = new Bundle(); + args.putInt(ARGS_KEY_LOADER, service.ordinal()); + + // check if we already had done some work on the loader + Loader loader = getSupportLoaderManager().getLoader(REQUEST_CODE_CLOUD_LIST_KEY); + if (loader != null && loader.isStarted()) { + + // making sure that loader is not started + getSupportLoaderManager().destroyLoader(REQUEST_CODE_CLOUD_LIST_KEY); + } + + getSupportLoaderManager().initLoader(REQUEST_CODE_CLOUD_LIST_KEY, args, this); + } + } catch (CloudPluginException e) { + LOG.warn("failure when adding cloud plugin connections", e); + Toast.makeText(this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) + .show(); + } + } + + @Override + public void deleteConnection(OpenMode service) { + cloudHandler.clear(service); + dataUtils.removeAccount(service); + + runOnUiThread(drawer::refreshDrawer); + } + + @NonNull + @Override + public Loader onCreateLoader(int id, Bundle args) { + Uri uri = + Uri.withAppendedPath( + Uri.parse("content://" + CloudContract.PROVIDER_AUTHORITY), "/keys.db/secret_keys"); + + String[] projection = + new String[] { + CloudContract.COLUMN_ID, + CloudContract.COLUMN_CLIENT_ID, + CloudContract.COLUMN_CLIENT_SECRET_KEY + }; + + switch (id) { + case REQUEST_CODE_CLOUD_LIST_KEY: + Uri uriAppendedPath = uri; + switch (OpenMode.getOpenMode(args.getInt(ARGS_KEY_LOADER, 2))) { + case GDRIVE: + uriAppendedPath = ContentUris.withAppendedId(uri, 2); + break; + case DROPBOX: + uriAppendedPath = ContentUris.withAppendedId(uri, 3); + break; + case BOX: + uriAppendedPath = ContentUris.withAppendedId(uri, 4); + break; + case ONEDRIVE: + uriAppendedPath = ContentUris.withAppendedId(uri, 5); + break; + } + return new CursorLoader(this, uriAppendedPath, projection, null, null, null); + case REQUEST_CODE_CLOUD_LIST_KEYS: + // we need a list of all secret keys - floatingActionButton.close(true); - return true; - } + try { + List cloudEntries = cloudHandler.getAllEntries(); - } + // we want keys for services saved in database, and the cloudrail app key which + // is at index 1 + String ids[] = new String[cloudEntries.size() + 1]; - /** - * Invoke {@link FtpServerFragment#changeFTPServerPath(String)} to change FTP server share path. - * - * @param dialog - * @param folder selected folder - * @see FtpServerFragment#changeFTPServerPath(String) - * @see FolderChooserDialog - * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback - */ - @Override - public void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File folder) { - switch (dialog.getTag()) { - case FtpServerFragment.TAG: - FtpServerFragment ftpServerFragment = (FtpServerFragment) getFragmentAtFrame(); - if (folder.exists() && folder.isDirectory()) { - if (FileUtils.isRunningAboveStorage(folder.getAbsolutePath())) { - if (!isRootExplorer()) { - AlertDialog.show( - this, - R.string.ftp_server_root_unavailable, - R.string.error, - android.R.string.ok, - null, - false); - } else { - MaterialDialog confirmDialog = - GeneralDialogCreation.showBasicDialog( - this, - R.string.ftp_server_root_filesystem_warning, - R.string.warning, - android.R.string.ok, - android.R.string.cancel); - confirmDialog - .getActionButton(DialogAction.POSITIVE) - .setOnClickListener( - v -> { - ftpServerFragment.changeFTPServerPath(folder.getPath()); - Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT) - .show(); - confirmDialog.dismiss(); - }); - confirmDialog - .getActionButton(DialogAction.NEGATIVE) - .setOnClickListener(v -> confirmDialog.dismiss()); - confirmDialog.show(); - } - } else { - ftpServerFragment.changeFTPServerPath(folder.getPath()); - Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); - } - } else { - // try to get parent - String pathParentFilePath = folder.getParent(); - if (pathParentFilePath == null) { - dialog.dismiss(); - return; - } - File pathParentFile = new File(pathParentFilePath); - if (pathParentFile.exists() && pathParentFile.isDirectory()) { - ftpServerFragment.changeFTPServerPath(pathParentFile.getPath()); - Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); - } else { - // don't have access, print error - Toast.makeText(this, R.string.ftp_path_change_error_invalid, Toast.LENGTH_SHORT).show(); - } - } - dialog.dismiss(); + ids[0] = 1 + ""; + for (int i = 1; i <= cloudEntries.size(); i++) { + + // we need to get only those cloud details which user wants + switch (cloudEntries.get(i - 1).getServiceType()) { + case GDRIVE: + ids[i] = 2 + ""; + break; + case DROPBOX: + ids[i] = 3 + ""; break; - default: - dialog.dismiss(); + case BOX: + ids[i] = 4 + ""; break; + case ONEDRIVE: + ids[i] = 5 + ""; + break; + } + } + return new CursorLoader(this, uri, projection, CloudContract.COLUMN_ID, ids, null); + } catch (CloudPluginException e) { + LOG.warn("failure when fetching cloud connections", e); + Toast.makeText( + this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) + .show(); } + default: + Uri undefinedUriAppendedPath = ContentUris.withAppendedId(uri, 7); + return new CursorLoader(this, undefinedUriAppendedPath, projection, null, null, null); } + } - /** - * Get whether list item is selected for action mode or not - * - * @return value - */ - public boolean getListItemSelected() { - return this.listItemSelected; - } - - public String getScrollToFileName() { - return this.scrollToFileName; + @Override + public void onLoadFinished(Loader loader, final Cursor data) { + if (data == null) { + Toast.makeText( + this, + getResources().getString(R.string.cloud_error_failed_restart), + Toast.LENGTH_LONG) + .show(); + return; } - /** - * Set list item selected value + /* + * This is hack for repeated calls to onLoadFinished(), + * we take the Cursor provided to check if the function + * has already been called on it. * - * @param value value + * TODO: find a fix for repeated callbacks to onLoadFinished() */ - public void setListItemSelected(boolean value) { - this.listItemSelected = value; + if (cloudCursorData != null && cloudCursorData == data) return; + cloudCursorData = data; + + if (cloudLoaderAsyncTask != null + && cloudLoaderAsyncTask.getStatus() == AsyncTask.Status.RUNNING) { + return; + } + cloudLoaderAsyncTask = new CloudLoaderAsyncTask(this, cloudHandler, cloudCursorData); + cloudLoaderAsyncTask.execute(); + } + + @Override + public void onLoaderReset(Loader loader) { + // For passing code check + } + + public void initCornersDragListener(boolean destroy, boolean shouldInvokeLeftAndRight) { + initBottomDragListener(destroy); + initLeftRightAndTopDragListeners(destroy, shouldInvokeLeftAndRight); + } + + private void initBottomDragListener(boolean destroy) { + View bottomPlaceholder = findViewById(R.id.placeholder_drag_bottom); + if (destroy) { + bottomPlaceholder.setOnDragListener(null); + bottomPlaceholder.setVisibility(View.GONE); + } else { + bottomPlaceholder.setVisibility(View.VISIBLE); + bottomPlaceholder.setOnDragListener( + new TabFragmentBottomDragListener( + () -> { + getCurrentMainFragment().smoothScrollListView(false); + return null; + }, + () -> { + getCurrentMainFragment().stopSmoothScrollListView(); + return null; + })); + } + } + + private void initLeftRightAndTopDragListeners(boolean destroy, boolean shouldInvokeLeftAndRight) { + TabFragment tabFragment = getTabFragment(); + tabFragment.initLeftRightAndTopDragListeners(destroy, shouldInvokeLeftAndRight); + } + + private static final class FabActionListener implements SpeedDialView.OnActionSelectedListener { + + MainActivity mainActivity; + SpeedDialView floatingActionButton; + + FabActionListener(MainActivity mainActivity) { + this.mainActivity = mainActivity; + this.floatingActionButton = mainActivity.floatingActionButton; } - /** - * Do nothing other than dismissing the folder selection dialog. - * - * @param dialog - * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback - */ @Override - public void onFolderChooserDismissed(@NonNull FolderChooserDialog dialog) { - dialog.dismiss(); - } - - private void executeWithMainFragment(@NonNull Function lambda) { - executeWithMainFragment(lambda, false); - } + public boolean onActionSelected(SpeedDialActionItem actionItem) { + final MainFragment ma = + (MainFragment) + ((TabFragment) + mainActivity.getSupportFragmentManager().findFragmentById(R.id.content_frame)) + .getCurrentTabFragment(); + final String path = ma.getCurrentPath(); + + switch (actionItem.getId()) { + case R.id.menu_new_folder: + mainActivity.mainActivityHelper.mkdir( + ma.getMainFragmentViewModel().getOpenMode(), path, ma); + break; + case R.id.menu_new_file: + mainActivity.mainActivityHelper.mkfile( + ma.getMainFragmentViewModel().getOpenMode(), path, ma); + break; + case R.id.menu_new_cloud: + BottomSheetDialogFragment fragment = new CloudSheetFragment(); + fragment.show( + ma.getActivity().getSupportFragmentManager(), CloudSheetFragment.TAG_FRAGMENT); + break; + } - @Nullable - private void executeWithMainFragment( - @NonNull Function lambda, boolean showToastIfMainFragmentIsNull) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment != null && mainFragment.getMainFragmentViewModel() != null) { - lambda.apply(mainFragment); - } else { - LOG.warn("MainFragment is null"); - if (showToastIfMainFragmentIsNull) { - AppConfig.toast(this, R.string.operation_unsuccesful); + floatingActionButton.close(true); + return true; + } + } + + /** + * Invoke {@link FtpServerFragment#changeFTPServerPath(String)} to change FTP server share path. + * + * @param dialog + * @param folder selected folder + * @see FtpServerFragment#changeFTPServerPath(String) + * @see FolderChooserDialog + * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback + */ + @Override + public void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File folder) { + switch (dialog.getTag()) { + case FtpServerFragment.TAG: + FtpServerFragment ftpServerFragment = (FtpServerFragment) getFragmentAtFrame(); + if (folder.exists() && folder.isDirectory()) { + if (FileUtils.isRunningAboveStorage(folder.getAbsolutePath())) { + if (!isRootExplorer()) { + AlertDialog.show( + this, + R.string.ftp_server_root_unavailable, + R.string.error, + android.R.string.ok, + null, + false); + } else { + MaterialDialog confirmDialog = + GeneralDialogCreation.showBasicDialog( + this, + R.string.ftp_server_root_filesystem_warning, + R.string.warning, + android.R.string.ok, + android.R.string.cancel); + confirmDialog + .getActionButton(DialogAction.POSITIVE) + .setOnClickListener( + v -> { + ftpServerFragment.changeFTPServerPath(folder.getPath()); + Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT) + .show(); + confirmDialog.dismiss(); + }); + confirmDialog + .getActionButton(DialogAction.NEGATIVE) + .setOnClickListener(v -> confirmDialog.dismiss()); + confirmDialog.show(); } + } else { + ftpServerFragment.changeFTPServerPath(folder.getPath()); + Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); + } + } else { + // try to get parent + String pathParentFilePath = folder.getParent(); + if (pathParentFilePath == null) { + dialog.dismiss(); + return; + } + File pathParentFile = new File(pathParentFilePath); + if (pathParentFile.exists() && pathParentFile.isDirectory()) { + ftpServerFragment.changeFTPServerPath(pathParentFile.getPath()); + Toast.makeText(this, R.string.ftp_path_change_success, Toast.LENGTH_SHORT).show(); + } else { + // don't have access, print error + Toast.makeText(this, R.string.ftp_path_change_error_invalid, Toast.LENGTH_SHORT).show(); + } } + dialog.dismiss(); + break; + default: + dialog.dismiss(); + break; + } + } + + /** + * Get whether list item is selected for action mode or not + * + * @return value + */ + public boolean getListItemSelected() { + return this.listItemSelected; + } + + public String getScrollToFileName() { + return this.scrollToFileName; + } + + /** + * 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. + * + * @param dialog + * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback + */ + @Override + public void onFolderChooserDismissed(@NonNull FolderChooserDialog dialog) { + dialog.dismiss(); + } + + private void executeWithMainFragment(@NonNull Function lambda) { + executeWithMainFragment(lambda, false); + } + + @Nullable + private void executeWithMainFragment( + @NonNull Function lambda, boolean showToastIfMainFragmentIsNull) { + final MainFragment mainFragment = getCurrentMainFragment(); + if (mainFragment != null && mainFragment.getMainFragmentViewModel() != null) { + lambda.apply(mainFragment); + } else { + LOG.warn("MainFragment is null"); + if (showToastIfMainFragmentIsNull) { + AppConfig.toast(this, R.string.operation_unsuccesful); + } } + } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index c792d5d00b..f6b1345d13 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -236,5 +236,4 @@ private void initSearchViewColor(MainActivity a) { public interface SearchListener { void onSearch(String queue); } - } From 7773e1171ed24c99f3d30612953445774e73c8ca Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Thu, 2 Feb 2023 22:55:19 +0800 Subject: [PATCH 044/384] Fix source tree test failures Follow-up fix for #3692. - Migrate tests that are still testing on JELLY_BEAN - Update submodules Android version requirement to 4.4 too --- .../asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt | 4 ++-- .../asynctasks/ssh/PemToKeyPairObservableRsaTest.kt | 4 ++-- commons_compress_7z/build.gradle | 2 +- file_operations/build.gradle | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt index f58cfdcc01..748e4e3732 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt @@ -20,7 +20,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh -import android.os.Build.VERSION_CODES.JELLY_BEAN +import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -44,7 +44,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowTabHandler::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P, VERSION_CODES.R] ) class PemToKeyPairObservableEd25519Test { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt index 1192f5e6ad..cf041dae5e 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.JELLY_BEAN +import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.N import android.os.Build.VERSION_CODES.P @@ -64,7 +64,7 @@ import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowTabHandler::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P, VERSION_CODES.R] ) class PemToKeyPairObservableRsaTest { diff --git a/commons_compress_7z/build.gradle b/commons_compress_7z/build.gradle index a550f9a908..0b7e3a9e9a 100644 --- a/commons_compress_7z/build.gradle +++ b/commons_compress_7z/build.gradle @@ -5,7 +5,7 @@ android { compileSdkVersion 28 defaultConfig { - minSdkVersion 14 + minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/file_operations/build.gradle b/file_operations/build.gradle index 3ead8fcc15..eadddd661e 100644 --- a/file_operations/build.gradle +++ b/file_operations/build.gradle @@ -5,7 +5,7 @@ android { compileSdkVersion 30 defaultConfig { - minSdkVersion 14 + minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" From 982c6c95926b47a3b125d28a704a711952fb3f78 Mon Sep 17 00:00:00 2001 From: peerzadaburhan Date: Fri, 3 Feb 2023 11:13:08 +0530 Subject: [PATCH 045/384] Issue #3394 --- .../java/com/amaze/filemanager/ui/activities/MainActivity.java | 1 + 1 file changed, 1 insertion(+) 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 1ebb3a8b9f..ad69cd4b3f 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 @@ -336,6 +336,7 @@ public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_toolbar); + intent = getIntent(); dataUtils = DataUtils.getInstance(); From 5cd4a1b923acf77dd79b1226a19c088ff17861ad Mon Sep 17 00:00:00 2001 From: peerzadaburhan Date: Fri, 3 Feb 2023 12:10:37 +0530 Subject: [PATCH 046/384] Issue #3394 --- .../java/com/amaze/filemanager/ui/activities/MainActivity.java | 1 - 1 file changed, 1 deletion(-) 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 ad69cd4b3f..1ebb3a8b9f 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 @@ -336,7 +336,6 @@ public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_toolbar); - intent = getIntent(); dataUtils = DataUtils.getInstance(); From ec30526fb336bd72735087a889e77ece952d9070 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 3 Feb 2023 15:29:56 +0530 Subject: [PATCH 047/384] fixes #3720 --- .../java/com/amaze/filemanager/ui/activities/MainActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 7bc4dd3221..4fee1ec4a5 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 @@ -2314,7 +2314,8 @@ public void onLoadFinished(Loader loader, final Cursor data) { * * TODO: find a fix for repeated callbacks to onLoadFinished() */ - if ((cloudCursorData != null && cloudCursorData == data) + if (cloudCursorData == null + || cloudCursorData == data || data.isClosed() || cloudCursorData.isClosed()) return; cloudCursorData = data; From 937e2f36d3b3902dd3ae8e041dbb137d5379c603 Mon Sep 17 00:00:00 2001 From: peerzadaburhan Date: Fri, 3 Feb 2023 15:34:42 +0530 Subject: [PATCH 048/384] Issue #3394 --- .../java/com/amaze/filemanager/ui/activities/MainActivity.java | 1 + 1 file changed, 1 insertion(+) 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 1ebb3a8b9f..ad69cd4b3f 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 @@ -336,6 +336,7 @@ public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_toolbar); + intent = getIntent(); dataUtils = DataUtils.getInstance(); From 8949382be5440e0da81bd9cb15a1408b947fc594 Mon Sep 17 00:00:00 2001 From: peerzadaburhan Date: Fri, 3 Feb 2023 19:00:04 +0530 Subject: [PATCH 049/384] Issue #3394 --- .../java/com/amaze/filemanager/ui/activities/MainActivity.java | 1 - 1 file changed, 1 deletion(-) 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 ad69cd4b3f..1ebb3a8b9f 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 @@ -336,7 +336,6 @@ public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_toolbar); - intent = getIntent(); dataUtils = DataUtils.getInstance(); From b2128bf6715627f5e7a7460559c6c968c6e20829 Mon Sep 17 00:00:00 2001 From: peerzadaburhan Date: Fri, 3 Feb 2023 19:22:12 +0530 Subject: [PATCH 050/384] Issue #3394 --- .../amaze/filemanager/ui/activities/MainActivity.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 1ebb3a8b9f..ff6c36469c 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 @@ -1271,8 +1271,8 @@ protected void onPostCreate(Bundle savedInstanceState) { @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - drawer.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggls + drawer.onConfigurationChanged(newConfig); } @Override @@ -2387,15 +2387,14 @@ public boolean onActionSelected(SpeedDialActionItem actionItem) { return true; } } - /** * Invoke {@link FtpServerFragment#changeFTPServerPath(String)} to change FTP server share path. * - * @param dialog - * @param folder selected folder * @see FtpServerFragment#changeFTPServerPath(String) * @see FolderChooserDialog * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback + * @param dialog + * @param folder selected folder */ @Override public void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File folder) { @@ -2487,8 +2486,8 @@ public void setListItemSelected(boolean value) { /** * Do nothing other than dismissing the folder selection dialog. * - * @param dialog * @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback + * @param dialog */ @Override public void onFolderChooserDismissed(@NonNull FolderChooserDialog dialog) { From 66b6a1f8927e2dba3d3936d13f7e6fb13a35a8e6 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 31 Dec 2022 10:53:31 +0800 Subject: [PATCH 051/384] Migrate FragmentStatePagerAdapter to FragmentStateAdapter Fixes #2790. - Migrate FragmentStatePagerAdapter in TabFragment to FragmentStateAdapter - Consequently, remove incompatible DisablableViewPager to ViewPager2 - Consequently, migrate incompatible OnPageChangeListener implementations to ViewPager2.OnPageChangeCallback, which is not interface and needs to be subclassed --- .../filemanager/ui/fragments/TabFragment.java | 121 +++++++++--------- .../ui/views/DisablableViewPager.java | 52 -------- .../amaze/filemanager/ui/views/Indicator.java | 80 ++++++------ app/src/main/res/layout/tabfragment.xml | 4 +- 4 files changed, 102 insertions(+), 155 deletions(-) delete mode 100644 app/src/main/java/com/amaze/filemanager/ui/views/DisablableViewPager.java 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 202cf01860..f37219154a 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 @@ -44,7 +44,6 @@ import com.amaze.filemanager.ui.drag.DragToTrashListener; import com.amaze.filemanager.ui.drag.TabFragmentSideDragListener; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; -import com.amaze.filemanager.ui.views.DisablableViewPager; import com.amaze.filemanager.ui.views.Indicator; import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.MainActivityHelper; @@ -68,11 +67,11 @@ import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.preference.PreferenceManager; -import androidx.viewpager.widget.ViewPager; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; -public class TabFragment extends Fragment implements ViewPager.OnPageChangeListener { +public class TabFragment extends Fragment { private final Logger LOG = LoggerFactory.getLogger(TabFragment.class); private static final String KEY_PATH = "path"; @@ -86,7 +85,7 @@ public class TabFragment extends Fragment implements ViewPager.OnPageChangeListe private final List fragments = new ArrayList<>(); private ScreenSlidePagerAdapter sectionsPagerAdapter; - private DisablableViewPager viewPager; + private ViewPager2 viewPager; private SharedPreferences sharedPrefs; private String path; @@ -132,9 +131,9 @@ public View onCreateView( } requireMainActivity().supportInvalidateOptionsMenu(); - viewPager.addOnPageChangeListener(this); + viewPager.registerOnPageChangeCallback(new OnPageChangeCallbackImpl()); - sectionsPagerAdapter = new ScreenSlidePagerAdapter(fragmentManager); + sectionsPagerAdapter = new ScreenSlidePagerAdapter(this); if (savedInstanceState == null) { int lastOpenTab = sharedPrefs.getInt(PREFERENCE_CURRENT_TAB, DEFAULT_CURRENT_TAB); MainActivity.currentTab = lastOpenTab; @@ -160,7 +159,7 @@ public View onCreateView( LOG.warn("failed to clear fragments", e); } - sectionsPagerAdapter = new ScreenSlidePagerAdapter(fragmentManager); + sectionsPagerAdapter = new ScreenSlidePagerAdapter(this); viewPager.setAdapter(sectionsPagerAdapter); int pos1 = savedInstanceState.getInt(KEY_POSITION, 0); @@ -251,82 +250,80 @@ public void onSaveInstanceState(@NonNull Bundle outState) { } } - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - final MainFragment mainFragment = requireMainActivity().getCurrentMainFragment(); - if (mainFragment == null - || mainFragment.getMainFragmentViewModel() == null - || mainFragment.getMainActivity().getListItemSelected()) { - return; // we do not want to update toolbar colors when ActionMode is activated - } - - // during the config change - @ColorInt int color = (int) evaluator.evaluate(position + positionOffset, startColor, endColor); + public void setPagingEnabled(boolean isPaging) { + viewPager.setUserInputEnabled(isPaging); + } - colorDrawable.setColor(color); - requireMainActivity().updateViews(colorDrawable); + public void setCurrentItem(int index) { + viewPager.setCurrentItem(index); } - @Override - public void onPageSelected(int p1) { - requireMainActivity() - .getAppbar() - .getAppbarLayout() - .animate() - .translationY(0) - .setInterpolator(new DecelerateInterpolator(2)) - .start(); + private class OnPageChangeCallbackImpl extends ViewPager2.OnPageChangeCallback { - MainActivity.currentTab = p1; + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + final MainFragment mainFragment = requireMainActivity().getCurrentMainFragment(); + if (mainFragment == null + || mainFragment.getMainFragmentViewModel() == null + || mainFragment.getMainActivity().getListItemSelected()) { + return; // we do not want to update toolbar colors when ActionMode is activated + } - if (sharedPrefs != null) { - sharedPrefs.edit().putInt(PREFERENCE_CURRENT_TAB, MainActivity.currentTab).apply(); - } + // during the config change + @ColorInt + int color = (int) evaluator.evaluate(position + positionOffset, startColor, endColor); - Fragment fragment = fragments.get(p1); - if (fragment instanceof MainFragment) { - MainFragment ma = (MainFragment) fragment; - if (ma.getCurrentPath() != null) { - requireMainActivity().getDrawer().selectCorrectDrawerItemForPath(ma.getCurrentPath()); - updateBottomBar(ma); - } + colorDrawable.setColor(color); + requireMainActivity().updateViews(colorDrawable); } - if (circleDrawable1 != null && circleDrawable2 != null) updateIndicator(p1); - } + @Override + public void onPageSelected(int p1) { + requireMainActivity() + .getAppbar() + .getAppbarLayout() + .animate() + .translationY(0) + .setInterpolator(new DecelerateInterpolator(2)) + .start(); + + MainActivity.currentTab = p1; + + if (sharedPrefs != null) { + sharedPrefs.edit().putInt(PREFERENCE_CURRENT_TAB, MainActivity.currentTab).apply(); + } - @Override - public void onPageScrollStateChanged(int state) { - // nothing to do - } + Fragment fragment = fragments.get(p1); + if (fragment instanceof MainFragment) { + MainFragment ma = (MainFragment) fragment; + if (ma.getCurrentPath() != null) { + requireMainActivity().getDrawer().selectCorrectDrawerItemForPath(ma.getCurrentPath()); + updateBottomBar(ma); + } + } - public void setPagingEnabled(boolean isPaging) { - viewPager.setPagingEnabled(isPaging); - } + if (circleDrawable1 != null && circleDrawable2 != null) updateIndicator(p1); + } - public void setCurrentItem(int index) { - viewPager.setCurrentItem(index); + @Override + public void onPageScrollStateChanged(int state) { + // nothing to do + } } - private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { - public ScreenSlidePagerAdapter(FragmentManager fm) { + private class ScreenSlidePagerAdapter extends FragmentStateAdapter { + public ScreenSlidePagerAdapter(Fragment fm) { super(fm); } @Override - public int getItemPosition(@NonNull Object object) { - int index = fragments.indexOf(object); - if (index == -1) return POSITION_NONE; - else return index; - } - - public int getCount() { + public int getItemCount() { return fragments.size(); } @NonNull @Override - public Fragment getItem(int position) { + public Fragment createFragment(int position) { Fragment f; f = fragments.get(position); return f; diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/DisablableViewPager.java b/app/src/main/java/com/amaze/filemanager/ui/views/DisablableViewPager.java deleted file mode 100644 index a04d283118..0000000000 --- a/app/src/main/java/com/amaze/filemanager/ui/views/DisablableViewPager.java +++ /dev/null @@ -1,52 +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.ui.views; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; - -import androidx.viewpager.widget.ViewPager; - -/** Created by Arpit on 16-01-2015. */ -public class DisablableViewPager extends ViewPager { - - private boolean enabled; - - public DisablableViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - this.enabled = true; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - return this.enabled && super.onTouchEvent(event); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - return this.enabled && super.onInterceptTouchEvent(event); - } - - public void setPagingEnabled(boolean enabled) { - this.enabled = enabled; - } -} diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/Indicator.java b/app/src/main/java/com/amaze/filemanager/ui/views/Indicator.java index 15ac5d0148..d740f74657 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/Indicator.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/Indicator.java @@ -30,7 +30,6 @@ import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; -import android.database.DataSetObserver; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -41,11 +40,12 @@ import android.view.View; import android.view.animation.Interpolator; +import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.ViewPager; +import androidx.viewpager2.widget.ViewPager2; /** An ink inspired widget for indicating pages in a {@link ViewPager}. */ -public class Indicator extends View - implements ViewPager.OnPageChangeListener, View.OnAttachStateChangeListener { +public class Indicator extends View implements View.OnAttachStateChangeListener { // defaults private static final int DEFAULT_DOT_SIZE = 8; // dp @@ -74,7 +74,7 @@ public class Indicator extends View private float dotBottomY; // ViewPager - private ViewPager viewPager; + private ViewPager2 viewPager; // state private int pageCount; @@ -165,57 +165,59 @@ public Indicator(Context context, AttributeSet attrs, int defStyle) { addOnAttachStateChangeListener(this); } - public void setViewPager(ViewPager viewPager) { + public void setViewPager(ViewPager2 viewPager) { this.viewPager = viewPager; - viewPager.addOnPageChangeListener(this); - setPageCount(viewPager.getAdapter().getCount()); + viewPager.registerOnPageChangeCallback(new OnPageChangeCallbackImpl()); + setPageCount(viewPager.getAdapter().getItemCount()); viewPager .getAdapter() - .registerDataSetObserver( - new DataSetObserver() { + .registerAdapterDataObserver( + new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { - setPageCount(Indicator.this.viewPager.getAdapter().getCount()); + setPageCount(Indicator.this.viewPager.getAdapter().getItemCount()); } }); setCurrentPageImmediate(); } - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - if (isAttachedToWindow) { - float fraction = positionOffset; - int currentPosition = pageChanging ? previousPage : currentPage; - int leftDotPosition = position; - // when swiping from #2 to #1 ViewPager reports position as 1 and a descending offset - // need to convert this into our left-dot-based 'coordinate space' - if (currentPosition != position) { - fraction = 1f - positionOffset; - - // if user scrolls completely to next page then the position param updates to that - // new page but we're not ready to switch our 'current' page yet so adjust for that - if (fraction == 1f) { - leftDotPosition = Math.min(currentPosition, position); + private class OnPageChangeCallbackImpl extends ViewPager2.OnPageChangeCallback { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (isAttachedToWindow) { + float fraction = positionOffset; + int currentPosition = pageChanging ? previousPage : currentPage; + int leftDotPosition = position; + // when swiping from #2 to #1 ViewPager reports position as 1 and a descending offset + // need to convert this into our left-dot-based 'coordinate space' + if (currentPosition != position) { + fraction = 1f - positionOffset; + + // if user scrolls completely to next page then the position param updates to that + // new page but we're not ready to switch our 'current' page yet so adjust for that + if (fraction == 1f) { + leftDotPosition = Math.min(currentPosition, position); + } } + setJoiningFraction(leftDotPosition, fraction); } - setJoiningFraction(leftDotPosition, fraction); } - } - @Override - public void onPageSelected(int position) { - if (isAttachedToWindow) { - // this is the main event we're interested in! - setSelectedPage(position); - } else { - // when not attached, don't animate the move, just store immediately - setCurrentPageImmediate(); + @Override + public void onPageSelected(int position) { + if (isAttachedToWindow) { + // this is the main event we're interested in! + setSelectedPage(position); + } else { + // when not attached, don't animate the move, just store immediately + setCurrentPageImmediate(); + } } - } - @Override - public void onPageScrollStateChanged(int state) { - // nothing to do + @Override + public void onPageScrollStateChanged(int state) { + // nothing to do + } } private void setPageCount(int pages) { diff --git a/app/src/main/res/layout/tabfragment.xml b/app/src/main/res/layout/tabfragment.xml index 2d6dda951f..d35e5d837a 100644 --- a/app/src/main/res/layout/tabfragment.xml +++ b/app/src/main/res/layout/tabfragment.xml @@ -21,7 +21,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> - --> - + Date: Sat, 31 Dec 2022 22:56:52 +0800 Subject: [PATCH 052/384] Changes per PR feedback - TabFragment Fix ScreenSlidePagerAdapter creation with FragmentManager and TabFragment's own lifecycle state - Added MainFragment.onSaveInstanceState() - Change ScreenSlidePagerAdapter to accept FragmentActivity instead --- .../amaze/filemanager/ui/fragments/MainFragment.java | 11 +++++++++++ .../amaze/filemanager/ui/fragments/TabFragment.java | 9 +++++---- 2 files changed, 16 insertions(+), 4 deletions(-) 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 a04ae74723..ca27991bd1 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 @@ -141,6 +141,8 @@ public class MainFragment extends Fragment AdjustListViewForTv { private static final Logger LOG = LoggerFactory.getLogger(MainFragment.class); + private static final String KEY_FRAGMENT_MAIN = "main"; + public SwipeRefreshLayout mSwipeRefreshLayout; public RecyclerAdapter adapter; @@ -285,6 +287,15 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat loadViews(); } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + FragmentManager fragmentManager = requireActivity().getSupportFragmentManager(); + fragmentManager.executePendingTransactions(); + fragmentManager.putFragment(outState, KEY_FRAGMENT_MAIN, this); + } + public void stopAnimation() { if ((!adapter.stoppedAnimation)) { for (int j = 0; j < listView.getChildCount(); j++) { 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 f37219154a..51bcc7bd4d 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 @@ -66,6 +66,7 @@ import androidx.annotation.NonNull; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; import androidx.viewpager2.adapter.FragmentStateAdapter; @@ -133,7 +134,7 @@ public View onCreateView( requireMainActivity().supportInvalidateOptionsMenu(); viewPager.registerOnPageChangeCallback(new OnPageChangeCallbackImpl()); - sectionsPagerAdapter = new ScreenSlidePagerAdapter(this); + sectionsPagerAdapter = new ScreenSlidePagerAdapter(requireActivity()); if (savedInstanceState == null) { int lastOpenTab = sharedPrefs.getInt(PREFERENCE_CURRENT_TAB, DEFAULT_CURRENT_TAB); MainActivity.currentTab = lastOpenTab; @@ -159,7 +160,7 @@ public View onCreateView( LOG.warn("failed to clear fragments", e); } - sectionsPagerAdapter = new ScreenSlidePagerAdapter(this); + sectionsPagerAdapter = new ScreenSlidePagerAdapter(requireActivity()); viewPager.setAdapter(sectionsPagerAdapter); int pos1 = savedInstanceState.getInt(KEY_POSITION, 0); @@ -312,8 +313,8 @@ public void onPageScrollStateChanged(int state) { } private class ScreenSlidePagerAdapter extends FragmentStateAdapter { - public ScreenSlidePagerAdapter(Fragment fm) { - super(fm); + public ScreenSlidePagerAdapter(FragmentActivity fragmentActivity) { + super(fragmentActivity); } @Override From d039c8b25480ba204f0dd7f215806034db3fe3ed Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 11 Feb 2023 21:04:07 +0530 Subject: [PATCH 053/384] store recent search items --- .../PreferencesConstants.kt | 3 + .../ui/views/appbar/SearchView.java | 114 +++++++++++++++--- app/src/main/res/layout-v21/layout_search.xml | 96 +++++++++++---- .../main/res/layout-w720dp/layout_search.xml | 90 ++++++++++---- app/src/main/res/layout/layout_search.xml | 92 ++++++++++---- app/src/main/res/values/styles.xml | 8 ++ 6 files changed, 319 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt index 346066b6ad..91978ffd33 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt @@ -92,6 +92,9 @@ object PreferencesConstants { const val PREFERENCE_EXPORT_SETTINGS = "export_settings" const val PREFERENCE_IMPORT_SETTINGS = "import_settings" + // recent search items + const val PREFERENCE_RECENT_SEARCH_ITEMS = "recent_searches" + // others const val PREFERENCE_CURRENT_TAB = "" const val PREFERENCE_BOOKMARKS_ADDED = "books_added" diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index f6b1345d13..6a45f34055 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -23,25 +23,35 @@ import static android.content.Context.INPUT_METHOD_SERVICE; import static android.os.Build.VERSION.SDK_INT; +import java.util.ArrayList; + import com.amaze.filemanager.R; import com.amaze.filemanager.ui.activities.MainActivity; +import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.theme.AppTheme; import com.amaze.filemanager.utils.Utils; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import android.animation.Animator; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.PorterDuff; +import android.view.ContextThemeWrapper; import android.view.View; import android.view.ViewAnimationUtils; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.ImageView; -import android.widget.RelativeLayout; +import android.widget.TextView; import androidx.appcompat.widget.AppCompatEditText; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; /** * SearchView, a simple view to search @@ -50,25 +60,33 @@ */ public class SearchView { - private MainActivity mainActivity; - private AppBar appbar; + private final MainActivity mainActivity; + private final AppBar appbar; + + private final ConstraintLayout searchViewLayout; + private final AppCompatEditText searchViewEditText; + private final ImageView clearImageView, backImageView; + private final TextView recentHintTV; + private final ChipGroup recentChipGroup; - private RelativeLayout searchViewLayout; - private AppCompatEditText searchViewEditText; - private ImageView clearImageView; - private ImageView backImageView; + private final SearchListener searchListener; private boolean enabled = false; - public SearchView( - final AppBar appbar, final MainActivity a, final SearchListener searchListener) { - mainActivity = a; + public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener searchListener) { + + this.mainActivity = mainActivity; + this.searchListener = searchListener; this.appbar = appbar; - searchViewLayout = a.findViewById(R.id.search_view); - searchViewEditText = a.findViewById(R.id.search_edit_text); - clearImageView = a.findViewById(R.id.search_close_btn); - backImageView = a.findViewById(R.id.img_view_back); + searchViewLayout = mainActivity.findViewById(R.id.search_view); + searchViewEditText = mainActivity.findViewById(R.id.search_edit_text); + clearImageView = mainActivity.findViewById(R.id.search_close_btn); + backImageView = mainActivity.findViewById(R.id.img_view_back); + recentChipGroup = mainActivity.findViewById(R.id.searchRecentItemsChipGroup); + recentHintTV = mainActivity.findViewById(R.id.searchRecentHintTV); + + initRecentSearches(mainActivity); clearImageView.setOnClickListener(v -> searchViewEditText.setText("")); @@ -77,16 +95,76 @@ public SearchView( searchViewEditText.setOnEditorActionListener( (v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEARCH) { - searchListener.onSearch(searchViewEditText.getText().toString()); + + String s = searchViewEditText.getText().toString(); + + searchListener.onSearch(s); appbar.getSearchView().hideSearchView(); + + String preferenceString = + PreferenceManager.getDefaultSharedPreferences(mainActivity) + .getString(PreferencesConstants.PREFERENCE_RECENT_SEARCH_ITEMS, null); + + ArrayList recentSearches = + preferenceString != null + ? new Gson() + .fromJson(preferenceString, new TypeToken>() {}.getType()) + : new ArrayList<>(); + + recentSearches.add(s); + + if (recentSearches.size() > 5) recentSearches.remove(0); + + PreferenceManager.getDefaultSharedPreferences(mainActivity) + .edit() + .putString( + PreferencesConstants.PREFERENCE_RECENT_SEARCH_ITEMS, + new Gson().toJson(recentSearches)) + .apply(); + + initRecentSearches(mainActivity); + return true; } return false; }); - initSearchViewColor(a); - // searchViewEditText.setTextColor(Utils.getColor(this, android.R.color.black)); - // searchViewEditText.setHintTextColor(Color.parseColor(ThemedActivity.accentSkin)); + initSearchViewColor(mainActivity); + } + + private void initRecentSearches(Context context) { + + String preferenceString = + PreferenceManager.getDefaultSharedPreferences(context) + .getString(PreferencesConstants.PREFERENCE_RECENT_SEARCH_ITEMS, null); + + if (preferenceString == null) { + recentHintTV.setVisibility(View.GONE); + recentChipGroup.setVisibility(View.GONE); + return; + } + + recentHintTV.setVisibility(View.VISIBLE); + recentChipGroup.setVisibility(View.VISIBLE); + + recentChipGroup.removeAllViews(); + + ArrayList recentSearches = + new Gson().fromJson(preferenceString, new TypeToken>() {}.getType()); + + for (String string : recentSearches) { + Chip chip = new Chip(new ContextThemeWrapper(context, R.style.ChipStyle)); + + chip.setText(string); + + recentChipGroup.addView(chip); + + chip.setOnClickListener( + v -> { + searchListener.onSearch(((Chip) v).getText().toString()); + appbar.getSearchView().hideSearchView(); + }); + } } /** show search view with a circular reveal animation */ diff --git a/app/src/main/res/layout-v21/layout_search.xml b/app/src/main/res/layout-v21/layout_search.xml index b37cbaa518..3757da7b63 100644 --- a/app/src/main/res/layout-v21/layout_search.xml +++ b/app/src/main/res/layout-v21/layout_search.xml @@ -1,47 +1,97 @@ - + android:visibility="gone"> + android:layout_marginRight="@dimen/search_view_back_margin_left_right" + android:background="@drawable/ripple" + android:src="@drawable/ic_arrow_back_black_24dp" + app:layout_constraintBottom_toBottomOf="@id/search_edit_text" + app:layout_constraintEnd_toStartOf="@id/search_edit_text" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/search_edit_text" /> + + app:layout_constraintBottom_toTopOf="@id/searchRecentHintTV" + app:layout_constraintEnd_toStartOf="@id/search_close_btn" + app:layout_constraintStart_toEndOf="@id/img_view_back" + app:layout_constraintTop_toTopOf="parent" /> + - \ No newline at end of file + android:layout_marginRight="@dimen/search_view_info_margin_left_right" + android:background="@drawable/ripple" + android:src="@drawable/ic_close_black_24dp" + app:layout_constraintBottom_toBottomOf="@id/search_edit_text" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/search_edit_text" + app:layout_constraintTop_toTopOf="@id/search_edit_text" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w720dp/layout_search.xml b/app/src/main/res/layout-w720dp/layout_search.xml index 09d90c2631..0da790bdf4 100644 --- a/app/src/main/res/layout-w720dp/layout_search.xml +++ b/app/src/main/res/layout-w720dp/layout_search.xml @@ -1,46 +1,94 @@ - + android:visibility="gone"> + android:layout_marginRight="@dimen/search_view_back_margin_left_right" + android:src="@drawable/ic_arrow_back_black_24dp" + app:layout_constraintBottom_toBottomOf="@id/search_edit_text" + app:layout_constraintEnd_toStartOf="@id/search_edit_text" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/search_edit_text" /> + + app:layout_constraintBottom_toTopOf="@id/searchRecentHintTV" + app:layout_constraintEnd_toStartOf="@id/search_close_btn" + app:layout_constraintStart_toEndOf="@id/img_view_back" + app:layout_constraintTop_toTopOf="parent" /> + - \ No newline at end of file + android:src="@drawable/ic_close_black_24dp" + app:layout_constraintBottom_toBottomOf="@id/search_edit_text" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/search_edit_text" + app:layout_constraintTop_toTopOf="@id/search_edit_text" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_search.xml b/app/src/main/res/layout/layout_search.xml index 5c32fc38bb..0da790bdf4 100644 --- a/app/src/main/res/layout/layout_search.xml +++ b/app/src/main/res/layout/layout_search.xml @@ -1,46 +1,94 @@ - - + android:visibility="gone"> + android:layout_marginRight="@dimen/search_view_back_margin_left_right" + android:src="@drawable/ic_arrow_back_black_24dp" + app:layout_constraintBottom_toBottomOf="@id/search_edit_text" + app:layout_constraintEnd_toStartOf="@id/search_edit_text" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/search_edit_text" /> + + app:layout_constraintBottom_toTopOf="@id/searchRecentHintTV" + app:layout_constraintEnd_toStartOf="@id/search_close_btn" + app:layout_constraintStart_toEndOf="@id/img_view_back" + app:layout_constraintTop_toTopOf="parent" /> + - + android:layout_marginRight="@dimen/search_view_info_margin_left_right" + android:src="@drawable/ic_close_black_24dp" + app:layout_constraintBottom_toBottomOf="@id/search_edit_text" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/search_edit_text" + app:layout_constraintTop_toTopOf="@id/search_edit_text" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 88b8898f4d..747c2be575 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -317,4 +317,12 @@ + + + From d0503942bcded27bbb3c04845a638df7fecb860c Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 18 Feb 2023 11:40:45 +0800 Subject: [PATCH 054/384] SDK 33 adaptations + lib upgrades It has to happen anyway. - Compile with SDK 33 - Use Kotlin 1.8 - Use Java 11 - Library upgrades - Fix syntax and API problems brought by the upgrade --- app/build.gradle | 19 +++++++++--------- .../ui/base/BaseBottomSheetFragment.kt | 4 ++-- .../ui/views/drawer/HasherOfMenuItem.kt | 2 +- .../database/MigrationTestHelper.java | 14 ++++++------- build.gradle | 20 +++++++++---------- commons_compress_7z/build.gradle | 2 +- file_operations/build.gradle | 4 ++-- 7 files changed, 32 insertions(+), 33 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 34cd3e4b97..c6a4ccd3b7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'com.hiya.jacoco-android' apply plugin: "com.starter.easylauncher" android { - compileSdkVersion 31 + compileSdkVersion 33 packagingOptions { resources { excludes += ['proguard-project.txt', 'project.properties', 'META-INF/LICENSE.txt', 'META-INF/LICENSE', 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/DEPENDENCIES.txt', 'META-INF/DEPENDENCIES'] @@ -17,7 +17,7 @@ android { defaultConfig { applicationId "com.amaze.filemanager" minSdkVersion 19 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 118 versionName "3.8.5" multiDexEnabled true @@ -85,13 +85,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = '1.8' - useIR = true + jvmTarget = '11' } testOptions { @@ -254,11 +253,11 @@ dependencies { configurations.all { resolutionStrategy { dependencySubstitution { - substitute module("commons-logging:commons-logging-api:1.1") with module("commons-logging:commons-logging:1.1.1") - substitute module("com.android.support:support-annotations:27.1.1") with module("com.android.support:support-annotations:27.0.2") + substitute module("commons-logging:commons-logging-api:1.1") using module("commons-logging:commons-logging:1.1.1") + substitute module("com.android.support:support-annotations:27.1.1") using module("com.android.support:support-annotations:27.0.2") // These two lines are added to prevent possible class clashes between awaitility (which uses hamcrest 2.1) and junit (which uses hamcrest 1.3). - substitute module('org.hamcrest:hamcrest-core:1.3') with module("org.hamcrest:hamcrest:2.1") - substitute module('org.hamcrest:hamcrest-library:1.3') with module("org.hamcrest:hamcrest:2.1") + substitute module('org.hamcrest:hamcrest-core:1.3') using module("org.hamcrest:hamcrest:2.1") + substitute module('org.hamcrest:hamcrest-library:1.3') using module("org.hamcrest:hamcrest:2.1") } } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/base/BaseBottomSheetFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/base/BaseBottomSheetFragment.kt index 78c3b6449f..c8e6551fa8 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/base/BaseBottomSheetFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/base/BaseBottomSheetFragment.kt @@ -39,7 +39,7 @@ open class BaseBottomSheetFragment : BottomSheetDialogFragment() { * Initializes bottom sheet ui resources based on current theme */ fun initDialogResources(rootView: View) { - when ((activity as ThemedActivity?)!!.appTheme!!) { + when ((requireActivity() as ThemedActivity).appTheme!!) { AppTheme.DARK -> { rootView.setBackgroundDrawable( context?.resources?.getDrawable( @@ -54,7 +54,7 @@ open class BaseBottomSheetFragment : BottomSheetDialogFragment() { ) ) } - AppTheme.LIGHT, AppTheme.TIMED -> { + AppTheme.LIGHT, AppTheme.TIMED, AppTheme.SYSTEM -> { rootView .setBackgroundDrawable( context?.resources?.getDrawable( diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/HasherOfMenuItem.kt b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/HasherOfMenuItem.kt index 1e09c91473..bbf04494d1 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/HasherOfMenuItem.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/HasherOfMenuItem.kt @@ -28,7 +28,7 @@ import android.view.MenuItem data class HasherOfMenuItem( val groupId: Int, val itemId: Int, - val title: CharSequence, + val title: CharSequence?, val ordering: Int ) diff --git a/app/src/test/java/com/amaze/filemanager/database/MigrationTestHelper.java b/app/src/test/java/com/amaze/filemanager/database/MigrationTestHelper.java index d5dcacc0c7..e5094cfa30 100644 --- a/app/src/test/java/com/amaze/filemanager/database/MigrationTestHelper.java +++ b/app/src/test/java/com/amaze/filemanager/database/MigrationTestHelper.java @@ -477,7 +477,7 @@ static class MigratingDelegate extends MigrationTestHelper.RoomOpenHelperDelegat } @Override - protected void createAllTables(SupportSQLiteDatabase database) { + public void createAllTables(SupportSQLiteDatabase database) { throw new UnsupportedOperationException( "Was expecting to migrate but received create." + "Make sure you have created the database first."); @@ -485,7 +485,7 @@ protected void createAllTables(SupportSQLiteDatabase database) { @NonNull @Override - protected RoomOpenHelper.ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) { + public RoomOpenHelper.ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) { final Map tables = mDatabaseBundle.getEntitiesByTableName(); for (EntityBundle entity : tables.values()) { if (entity instanceof FtsEntityBundle) { @@ -548,7 +548,7 @@ static class CreatingDelegate extends MigrationTestHelper.RoomOpenHelperDelegate } @Override - protected void createAllTables(SupportSQLiteDatabase database) { + public void createAllTables(SupportSQLiteDatabase database) { for (String query : mDatabaseBundle.buildCreateQueries()) { database.execSQL(query); } @@ -556,7 +556,7 @@ protected void createAllTables(SupportSQLiteDatabase database) { @NonNull @Override - protected RoomOpenHelper.ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) { + public RoomOpenHelper.ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) { throw new UnsupportedOperationException( "This open helper just creates the database but" + " it received a migration request."); } @@ -571,14 +571,14 @@ abstract static class RoomOpenHelperDelegate extends RoomOpenHelper.Delegate { } @Override - protected void dropAllTables(SupportSQLiteDatabase database) { + public void dropAllTables(SupportSQLiteDatabase database) { throw new UnsupportedOperationException("cannot drop all tables in the test"); } @Override - protected void onCreate(SupportSQLiteDatabase database) {} + public void onCreate(SupportSQLiteDatabase database) {} @Override - protected void onOpen(SupportSQLiteDatabase database) {} + public void onOpen(SupportSQLiteDatabase database) {} } } diff --git a/build.gradle b/build.gradle index 94f25bf6b9..18909ccf8e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,23 +2,23 @@ buildscript { ext { - kotlin_version = "1.6.10" + kotlin_version = "1.8.0" robolectricVersion = '4.9' glideVersion = '4.11.0' sshjVersion = '0.34.0' jcifsVersion = '2.1.6' fabSpeedDialVersion = '3.1.1' - roomVersion = '2.4.3' + roomVersion = '2.5.0' bouncyCastleVersion = '1.70' awaitilityVersion = "3.1.6" androidXCoreVersion = "1.7.0" - androidMaterialVersion = "1.4.0" // Upgrade to 1.5 requires targetSdkVersion 31 - androidXFragmentVersion = "1.4.1" - androidXAppCompatVersion = "1.4.1" - androidXAnnotationVersion = "1.3.0" + androidMaterialVersion = "1.5.0" // Upgrade to 1.5 requires targetSdkVersion 31 + androidXFragmentVersion = "1.5.5" + androidXAppCompatVersion = "1.6.1" + androidXAnnotationVersion = "1.5.0" androidXPrefVersion = "1.2.0" - androidXTestVersion = "1.4.0" - androidXTestExtVersion = "1.1.3" + androidXTestVersion = "1.5.0" + androidXTestExtVersion = "1.1.5" uiAutomatorVersion = "2.2.0" junitVersion = "4.13.2" slf4jVersion = "1.7.25" @@ -27,7 +27,7 @@ buildscript { androidBillingVersion = "5.0.0" junrarVersion = "7.4.0" zip4jVersion = "2.6.4" - espressoVersion = "3.4.0" + espressoVersion = "3.5.1" materialDialogsVersion = "0.9.6.0" jacocoVersion = "0.8.7" commonsCompressVersion = "1.22" @@ -66,7 +66,7 @@ allprojects { maven { url "https://jcenter.bintray.com" } } tasks.withType(Test) { - maxParallelForks = 4 + maxParallelForks = 8 maxHeapSize = "2g" forkEvery = 4 failFast = true diff --git a/commons_compress_7z/build.gradle b/commons_compress_7z/build.gradle index 0b7e3a9e9a..428e80db73 100644 --- a/commons_compress_7z/build.gradle +++ b/commons_compress_7z/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion 33 defaultConfig { minSdkVersion 19 diff --git a/file_operations/build.gradle b/file_operations/build.gradle index eadddd661e..47cefa5e63 100644 --- a/file_operations/build.gradle +++ b/file_operations/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 33 defaultConfig { minSdkVersion 19 @@ -51,7 +51,7 @@ android { } kotlinOptions { - useIR = true + jvmTarget = '11' } } From adca402664794238724c7ee345a7bfa7d74afeaf Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Mon, 28 Nov 2022 22:11:03 +0800 Subject: [PATCH 055/384] Remove JUnit test report until further notice --- .github/workflows/android-feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android-feature.yml b/.github/workflows/android-feature.yml index dc1cc3f8f5..c661c27df0 100644 --- a/.github/workflows/android-feature.yml +++ b/.github/workflows/android-feature.yml @@ -44,4 +44,4 @@ jobs: - name: Run test cases uses: gradle/gradle-build-action@v2 with: - arguments: jacocoTestPlayDebugUnitTestReport --stacktrace --info + arguments: jacocoTestPlayDebugUnitTestReport --stacktrace --info \ No newline at end of file From 25e9bc7be066bc16e4382dbcc7986b3fb2cd01e8 Mon Sep 17 00:00:00 2001 From: TranceLove Date: Fri, 28 Oct 2022 18:25:16 +0800 Subject: [PATCH 056/384] ConnectionInfo improvements - Decouple from NetCopyClientConnectionPool to individual class - Use proven regex to parse URI instead of mangling substrings all the time - Defer decrypting password until authentication callables In design it can handle the path part in URL encoded string, but seems the regex is enough to grep the parts, so let it be. --- .../ftp/auth/FtpAuthenticationTaskCallable.kt | 12 +- .../auth/FtpsAuthenticationTaskCallable.kt | 7 +- .../ssh/SshAuthenticationTaskCallable.kt | 12 +- .../filemanager/database/UtilitiesDatabase.kt | 4 +- .../filemanager/filesystem/HybridFile.java | 5 +- .../ftp/NetCopyClientConnectionPool.kt | 98 +--- .../filesystem/ftp/NetCopyClientUtils.kt | 115 ++-- .../filesystem/ftp/NetCopyConnectionInfo.kt | 160 ++++++ .../ui/activities/MainActivity.java | 26 +- .../com/amaze/filemanager/utils/GenericExt.kt | 22 + .../filemanager/utils/MainActivityHelper.java | 16 +- .../com/amaze/filemanager/utils/SmbUtil.kt | 55 +- .../asynctasks/MoveFilesTest.java | 102 ---- .../ssh/SshAuthenticationTaskTest.kt | 2 +- .../UtilitiesDatabaseMigrationTest.kt | 46 +- .../ftp/NetCopyClientConnectionPoolFtpTest.kt | 261 ++------- .../filesystem/ftp/NetCopyClientUtilTest.kt | 2 +- .../ftp/NetCopyConnectionInfoTest.kt | 306 ++++++++++ .../ssh/AbstractSftpServerTest.java | 127 ----- .../filesystem/ssh/AbstractSftpServerTest.kt | 162 ++++++ .../filesystem/ssh/CreateFileOnSshdTest.java | 64 --- .../filesystem/ssh/ListFilesOnSshdTest.java | 183 ------ .../filesystem/ssh/ListFilesOnSshdTest.kt | 245 ++++++++ .../ssh/NetCopyClientConnectionPoolSshTest.kt | 524 ++++-------------- .../filesystem/ssh/test/TestUtils.kt | 11 +- .../ui/activities/MainActivityTest.java | 59 +- .../amaze/filemanager/utils/SmbUtilTest.kt | 2 - 27 files changed, 1259 insertions(+), 1369 deletions(-) create mode 100644 app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt delete mode 100644 app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/MoveFilesTest.java create mode 100644 app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt delete mode 100644 app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java create mode 100644 app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt delete mode 100644 app/src/test/java/com/amaze/filemanager/filesystem/ssh/CreateFileOnSshdTest.java delete mode 100644 app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.java create mode 100644 app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.kt diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTaskCallable.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTaskCallable.kt index 3032912874..fdd5784a3a 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTaskCallable.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTaskCallable.kt @@ -21,13 +21,17 @@ package com.amaze.filemanager.asynchronous.asynctasks.ftp.auth import androidx.annotation.WorkerThread +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.ftp.FTPClientImpl import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.CONNECT_TIMEOUT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX +import com.amaze.filemanager.utils.PasswordUtil import net.schmizz.sshj.userauth.UserAuthException import org.apache.commons.net.ftp.FTPClient +import java.net.URLDecoder.decode import java.util.concurrent.Callable +import kotlin.text.Charsets.UTF_8 open class FtpAuthenticationTaskCallable( protected val hostname: String, @@ -48,7 +52,13 @@ open class FtpAuthenticationTaskCallable( FTPClientImpl.generateRandomEmailAddressForLogin() ) } else { - ftpClient.login(username, password) + ftpClient.login( + username, + decode( + PasswordUtil.decryptPassword(AppConfig.getInstance(), password), + UTF_8.name() + ) + ) } return if (loginSuccess) { ftpClient.enterLocalPassiveMode() diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpsAuthenticationTaskCallable.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpsAuthenticationTaskCallable.kt index 206edcdff9..5d28d1d9e9 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpsAuthenticationTaskCallable.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpsAuthenticationTaskCallable.kt @@ -20,9 +20,11 @@ package com.amaze.filemanager.asynchronous.asynctasks.ftp.auth +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.ftp.FTPClientImpl import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX +import com.amaze.filemanager.utils.PasswordUtil import com.amaze.filemanager.utils.X509CertificateUtil import com.amaze.filemanager.utils.X509CertificateUtil.FINGERPRINT import net.schmizz.sshj.userauth.UserAuthException @@ -50,7 +52,10 @@ class FtpsAuthenticationTaskCallable( FTPClientImpl.generateRandomEmailAddressForLogin() ) } else { - ftpClient.login(username, password) + ftpClient.login( + username, + PasswordUtil.decryptPassword(AppConfig.getInstance(), password) + ) } return if (loginSuccess) { // RFC 2228 set protection buffer size to 0 diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskCallable.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskCallable.kt index 4d1c15c333..5859931b30 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskCallable.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskCallable.kt @@ -20,15 +20,19 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool import com.amaze.filemanager.filesystem.ssh.CustomSshJConfig +import com.amaze.filemanager.utils.PasswordUtil import net.schmizz.sshj.SSHClient import net.schmizz.sshj.common.KeyType import net.schmizz.sshj.userauth.keyprovider.KeyProvider +import java.net.URLDecoder.decode import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey import java.util.concurrent.Callable +import kotlin.text.Charsets.UTF_8 class SshAuthenticationTaskCallable( private val hostname: String, @@ -68,7 +72,13 @@ class SshAuthenticationTaskCallable( ) sshClient } else { - sshClient.authPassword(username, password) + sshClient.authPassword( + username, + decode( + PasswordUtil.decryptPassword(AppConfig.getInstance(), password!!), + UTF_8.name() + ) + ) sshClient } } diff --git a/app/src/main/java/com/amaze/filemanager/database/UtilitiesDatabase.kt b/app/src/main/java/com/amaze/filemanager/database/UtilitiesDatabase.kt index eb059f5686..d61b7faad1 100644 --- a/app/src/main/java/com/amaze/filemanager/database/UtilitiesDatabase.kt +++ b/app/src/main/java/com/amaze/filemanager/database/UtilitiesDatabase.kt @@ -43,8 +43,8 @@ import com.amaze.filemanager.database.models.utilities.Hidden 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.ftp.NetCopyClientConnectionPool.AT -import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.COLON +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.AT +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLON import com.amaze.filemanager.utils.PasswordUtil.decryptPassword import com.amaze.filemanager.utils.PasswordUtil.encryptPassword import org.slf4j.LoggerFactory 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 39823e6391..f768d9e22c 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -69,8 +69,8 @@ import com.amaze.filemanager.filesystem.ftp.ExtensionsKt; import com.amaze.filemanager.filesystem.ftp.FTPClientImpl; import com.amaze.filemanager.filesystem.ftp.FtpClientTemplate; -import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool; import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils; +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo; import com.amaze.filemanager.filesystem.root.DeleteFileCommand; import com.amaze.filemanager.filesystem.root.ListFilesCommand; import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate; @@ -1054,8 +1054,7 @@ public static String parseAndFormatUriForDisplay(@NonNull String uriString) { if (uriString.startsWith(SSH_URI_PREFIX) || uriString.startsWith(FTP_URI_PREFIX) || uriString.startsWith(FTPS_URI_PREFIX)) { - NetCopyClientConnectionPool.ConnectionInfo connInfo = - new NetCopyClientConnectionPool.ConnectionInfo(uriString); + NetCopyConnectionInfo connInfo = new NetCopyConnectionInfo(uriString); return connInfo.toString(); } else { Uri uri = Uri.parse(uriString); diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt index f3fd9b928d..fb9c8837e5 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt @@ -26,7 +26,6 @@ import com.amaze.filemanager.asynchronous.asynctasks.ftp.auth.FtpAuthenticationT import com.amaze.filemanager.asynchronous.asynctasks.ssh.PemToKeyPairObservable import com.amaze.filemanager.asynchronous.asynctasks.ssh.SshAuthenticationTask import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils.extractBaseUriFrom -import com.amaze.filemanager.utils.PasswordUtil import io.reactivex.Flowable import io.reactivex.Maybe import io.reactivex.Observable.create @@ -56,9 +55,6 @@ object NetCopyClientConnectionPool { const val FTPS_URI_PREFIX = "ftps://" const val SSH_URI_PREFIX = "ssh://" const val CONNECT_TIMEOUT = 30000 - const val AT = '@' - const val SLASH = '/' - const val COLON = ':' private var connections: MutableMap> = ConcurrentHashMap() @@ -251,7 +247,7 @@ object NetCopyClientConnectionPool { // key-based authentication @Suppress("TooGenericExceptionThrown") private fun createSshClient(url: String): NetCopyClient? { - val connInfo = ConnectionInfo(url) + val connInfo = NetCopyConnectionInfo(url) val utilsHandler = AppConfig.getInstance().utilsHandler val pem = utilsHandler.getSshAuthPrivateKey(url) val keyPair = AtomicReference(null) @@ -340,7 +336,7 @@ object NetCopyClientConnectionPool { } private fun createFtpClient(url: String): NetCopyClient? { - ConnectionInfo(url).run { + NetCopyConnectionInfo(url).run { val certInfo = if (FTPS_URI_PREFIX == prefix) { AppConfig.getInstance().utilsHandler.getRemoteHostKey(url) } else { @@ -391,96 +387,6 @@ object NetCopyClientConnectionPool { } } - /** - * Container object for SSH URI, encapsulating logic for splitting information from given URI. - * `Uri.parse()` only parse URI that is compliant to RFC2396, but we have to deal with - * URI that is not compliant, since usernames and/or strong passwords usually have special - * characters included, like `ssh://user@example.com:P@##w0rd@127.0.0.1:22`. - * - * A design decision to keep database schema slim, by the way... -TranceLove - */ - internal class ConnectionInfo(url: String) { - val prefix: String - val host: String - val port: Int - val username: String - val password: String? - var defaultPath: String? = null - var queryString: String? = null - - // FIXME: Crude assumption - init { - require( - url.startsWith(SSH_URI_PREFIX) or - url.startsWith(FTP_URI_PREFIX) or - url.startsWith(FTPS_URI_PREFIX) - ) { - "Argument is not a SSH URI: $url" - } - host = if (url.contains(AT)) { - url.substring(url.lastIndexOf(AT) + 1, url.lastIndexOf(COLON)) - } else { - url.substring(url.lastIndexOf("//") + 2, url.lastIndexOf(COLON)) - } - val portAndPath = url.substring(url.lastIndexOf(COLON) + 1) - var port: Int - if (portAndPath.contains(SLASH)) { - port = portAndPath.substring(0, portAndPath.indexOf(SLASH)).toInt() - defaultPath = portAndPath.substring(portAndPath.indexOf(SLASH)) - } else { - port = portAndPath.toInt() - defaultPath = null - } - // If the uri is fetched from the app's database storage, we assume it will never be empty - prefix = when { - url.startsWith(SSH_URI_PREFIX) -> SSH_URI_PREFIX - url.startsWith(FTPS_URI_PREFIX) -> FTPS_URI_PREFIX - else -> FTP_URI_PREFIX - } - if (prefix != SSH_URI_PREFIX && !url.contains(AT)) { - username = "" - password = "" - } else { - val authString = url.substring(prefix.length, url.lastIndexOf(AT)) - val userInfo = authString.split(":").toTypedArray() - username = userInfo[0] - password = if (userInfo.size > 1) { - runCatching { - PasswordUtil.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 = if (url.startsWith(SSH_URI_PREFIX)) { - SSH_DEFAULT_PORT - } else { - FTP_DEFAULT_PORT - } - this.port = port - this.queryString = if (url.contains('?')) { - url.substringAfter('?') - } else { - null - } - } - - override fun toString(): String { - return if (username != "") { - "$prefix$username@$host:$port${defaultPath ?: ""}" - } else { - "$prefix$host:$port${defaultPath ?: ""}" - } - } - } - class AsyncRemoveConnection internal constructor( private val url: String ) : Callable { diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt index ae9c258452..ed3c5ce77e 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt @@ -26,11 +26,17 @@ import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.fileoperations.filesystem.DOESNT_EXIST import com.amaze.filemanager.fileoperations.filesystem.FolderState import com.amaze.filemanager.fileoperations.filesystem.WRITABLE_ON_REMOTE -import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.AT -import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.COLON -import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SLASH +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_DEFAULT_PORT +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_DEFAULT_PORT +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_DEFAULT_PORT import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.getConnection +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.AT +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLON +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.SLASH +import com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate import com.amaze.filemanager.utils.SmbUtil import io.reactivex.Maybe @@ -41,8 +47,6 @@ import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPReply import org.slf4j.LoggerFactory import java.io.IOException -import java.net.URLDecoder -import java.security.GeneralSecurityException import java.security.KeyPair object NetCopyClientUtils { @@ -109,15 +113,16 @@ object NetCopyClientUtils { * @return SSH URL with the password (if exists) encrypted */ fun encryptFtpPathAsNecessary(fullUri: String): String { - val prefix = fullUri.substring(0, fullUri.indexOf("://") + 3) - val uriWithoutProtocol: String = fullUri.substring(prefix.length) - return if (uriWithoutProtocol.substringBefore(AT).indexOf(COLON) > 0) { - SmbUtil.getSmbEncryptedPath( - AppConfig.getInstance(), + return NetCopyConnectionInfo(fullUri).run { + val uriWithoutProtocol: String = fullUri.substring(prefix.length) + if (uriWithoutProtocol.substringBefore(AT).indexOf(COLON) > 0) { + SmbUtil.getSmbEncryptedPath( + AppConfig.getInstance(), + fullUri + ) + } else { fullUri - ).replace("\n", "") - } else { - fullUri + } } } @@ -128,18 +133,14 @@ object NetCopyClientUtils { * @param fullUri SSH URL * @return SSH URL with the password (if exists) decrypted */ - fun decryptFtpPathAsNecessary(fullUri: String): String? { - val prefix = fullUri.substring(0, fullUri.indexOf("://") + 3) - val uriWithoutProtocol: String = fullUri.substring(prefix.length) - return try { + fun decryptFtpPathAsNecessary(fullUri: String): String { + return NetCopyConnectionInfo(fullUri).runCatching { + val uriWithoutProtocol: String = fullUri.substring(prefix.length) if (uriWithoutProtocol.lastIndexOf(COLON) > 0) SmbUtil.getSmbDecryptedPath( AppConfig.getInstance(), fullUri ) else fullUri - } catch (e: IOException) { - LOG.error("Error decrypting path", e) - fullUri - } catch (e: GeneralSecurityException) { + }.getOrElse { e -> LOG.error("Error decrypting path", e) fullUri } @@ -156,30 +157,21 @@ object NetCopyClientUtils { * @return The remote path part of the full SSH URL */ fun extractBaseUriFrom(fullUri: String): String { - val prefix = fullUri.substring(0, fullUri.indexOf("://") + 3) - val uriWithoutProtocol: String = fullUri.substring(prefix.length) - val credentials: String - val hostAndPath: String - if (uriWithoutProtocol.contains(AT)) { - credentials = uriWithoutProtocol.substring(0, uriWithoutProtocol.lastIndexOf(AT)) - hostAndPath = uriWithoutProtocol.substring(uriWithoutProtocol.lastIndexOf(AT) + 1) - } else { - credentials = "" - hostAndPath = uriWithoutProtocol - } - return if (hostAndPath.indexOf(SLASH) == -1) { - fullUri - } else { - val host = hostAndPath.substring(0, hostAndPath.indexOf(SLASH)) - val credentialsLen = if (credentials == "") { - 0 - } else { - credentials.length + 1 + return NetCopyConnectionInfo(fullUri).let { + buildString { + append(it.prefix) + append(it.username.ifEmpty { "" }) + if (true == it.password?.isNotEmpty()) { + append(COLON).append(it.password) + } + if (it.username.isNotEmpty()) { + append(AT) + } + append(it.host) + if (it.port > 0) { + append(COLON).append(it.port) + } } - fullUri.substring( - 0, - prefix.length + credentialsLen + host.length - ) } } @@ -194,20 +186,14 @@ object NetCopyClientUtils { * @return The remote path part of the full SSH URL */ fun extractRemotePathFrom(fullUri: String): String { - if (fullUri.contains(AT)) { - val hostPath = fullUri.substring(fullUri.lastIndexOf(AT)) - return if (hostPath.indexOf(SLASH) == -1) { - SLASH.toString() - } else { - URLDecoder.decode( - hostPath.substring(hostPath.indexOf(SLASH)), - Charsets.UTF_8.name() - ) - } - } else { - val hostAndPath = fullUri.substringAfter("://") - return if (hostAndPath.contains(SLASH)) { - hostAndPath.substring(hostAndPath.indexOf(SLASH)) + return NetCopyConnectionInfo(fullUri).let { connInfo -> + if (true == connInfo.defaultPath?.isNotEmpty()) { + buildString { + append(connInfo.defaultPath) + if (true == connInfo.filename?.isNotEmpty()) { + append(SLASH).append(connInfo.filename) + } + } } else { SLASH.toString() } @@ -283,4 +269,17 @@ object NetCopyClientUtils { } return execute(template) ?: DOESNT_EXIST } + + /** + * Return the default port used by different protocols. + * + * Reserved for future use. + */ + fun defaultPort(prefix: String) = when (prefix) { + SSH_URI_PREFIX -> SSH_DEFAULT_PORT + FTPS_URI_PREFIX -> FTPS_DEFAULT_PORT + FTP_URI_PREFIX -> FTP_DEFAULT_PORT + SMB_URI_PREFIX -> 0 // SMB never requires explicit port number at URL + else -> throw IllegalArgumentException("Cannot derive default port") + } } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt new file mode 100644 index 0000000000..ea61465d99 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2014-2022 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.filesystem.ftp + +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLON +import com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX +import java.net.URLDecoder.decode + +/** + * Container object for SSH/FTP/FTPS URL, encapsulating logic for splitting information from given + * URL. `Uri.parse()` only parse URL that is compliant to RFC2396, but we have to deal with + * URL that is not compliant, since usernames and/or strong passwords usually have special + * characters included, like `ssh://user@example.com:P@##w0rd@127.0.0.1:22`. + * + * A design decision to keep database schema slim, by the way... -TranceLove + * + * @param url URI to break down. + * + * For credentials, can be base64 or URL encoded, but if both username and password is provided, + * must use plain colon character [COLON] as separator. + * + * For paths and query strings, **always** use URL encoded paths, or undesired behaviour will + * occur. No validation is made at this point, so proceed at your own risk. + */ +class NetCopyConnectionInfo(url: String) { + + val prefix: String + val host: String + val port: Int + val username: String + val password: String? + var defaultPath: String? = null + private set + var queryString: String? = null + private set + var arguments: Map? + private set + var filename: String? = null + private set + + companion object { + // Regex taken from https://blog.stevenlevithan.com/archives/parseuri + // (No, don't break it down to lines) + + /* ktlint-disable max-line-length */ + private const val URI_REGEX = "^(?:(?![^:@]+:[^:@\\/]*@)([^:\\/?#.]+):)?(?:\\/\\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\\/?#]*)(?::(\\d*))?)(((\\/(?:[^?#](?![^?#\\/]*\\.[^?#\\/.]+(?:[?#]|$)))*\\/?)?([^?#\\/]*))(?:\\?([^#]*))?(?:#(.*))?)" + + /* ktlint-enable max-line-length */ + const val AND = '&' + const val AT = '@' + const val SLASH = '/' + const val COLON = ':' + } + + init { + require( + url.startsWith(SSH_URI_PREFIX) or + url.startsWith(FTP_URI_PREFIX) or + url.startsWith(FTPS_URI_PREFIX) or + url.startsWith(SMB_URI_PREFIX) + ) { + "Argument is not a SSH URI: $url" + } + val regex = Regex(URI_REGEX) + val matches = regex.find(url) + if (matches == null) { + throw IllegalArgumentException("Unable to parse URI") + } else { + matches.groupValues.let { + prefix = "${it[1]}://" + host = it[6] + val credential = it[3] + if (!credential.contains(COLON)) { + username = decode(credential, Charsets.UTF_8.name()) + password = null + } else { + username = decode(credential.substringBefore(COLON), Charsets.UTF_8.name()) + password = decode(credential.substringAfter(COLON), Charsets.UTF_8.name()) + } + port = if (it[7].isNotEmpty()) { + /* + * Invalid string would have been trapped to other branches. Strings fell into + * this branch must be integer + */ + it[7].toInt() + } else { + 0 + } + queryString = it[12].ifEmpty { null } + arguments = if (it[12].isNotEmpty()) { + it[12].split(AND).associate { valuePair -> + val pair = valuePair.split('=') + Pair( + pair[0], + pair[1].ifEmpty { + "" + } + ) + } + } else { + null + } + defaultPath = if (it[9].isEmpty()) { + null + } else if (it[9] == SLASH.toString()) { + SLASH.toString() + } else if (!it[9].endsWith(SLASH)) { + if (it[11].isEmpty()) { + it[10] + } else { + it[10].substringBeforeLast(SLASH) + } + } else { + it[9] + } + filename = it[11].ifEmpty { null } + } + } + } + + /** + * Returns the last segment of the URL's path element. + */ + fun lastPathSegment(): String? { + return if (filename != null && true == filename?.isNotEmpty()) { + filename + } else if (defaultPath != null && true == defaultPath?.isNotEmpty()) { + defaultPath!!.substringAfterLast(SLASH) + } else null + } + + override fun toString(): String { + return if (username.isNotEmpty()) { + "$prefix$username@$host${if (port == 0) "" else ":$port"}${defaultPath ?: ""}" + } else { + "$prefix$host${if (port == 0) "" else ":$port"}${defaultPath ?: ""}" + } + } +} 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 4fee1ec4a5..80a35caf0d 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 @@ -101,6 +101,7 @@ import com.amaze.filemanager.filesystem.RootHelper; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool; +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo; import com.amaze.filemanager.filesystem.ssh.SshClientUtils; import com.amaze.filemanager.ui.ExtensionsKt; import com.amaze.filemanager.ui.activities.superclasses.PermissionsActivity; @@ -132,6 +133,7 @@ import com.amaze.filemanager.utils.AppConstants; import com.amaze.filemanager.utils.BookSorter; import com.amaze.filemanager.utils.DataUtils; +import com.amaze.filemanager.utils.GenericExtKt; import com.amaze.filemanager.utils.MainActivityActionMode; import com.amaze.filemanager.utils.MainActivityHelper; import com.amaze.filemanager.utils.OTGUtil; @@ -203,6 +205,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import kotlin.collections.ArraysKt; +import kotlin.jvm.functions.Function1; +import kotlin.text.Charsets; public class MainActivity extends PermissionsActivity implements SmbConnectionListener, @@ -1985,7 +1990,7 @@ public void showSftpDialog(String name, String path, boolean edit) { } SftpConnectDialog sftpConnectDialog = new SftpConnectDialog(); String finalName = name; - Flowable.fromCallable(() -> new NetCopyClientConnectionPool.ConnectionInfo(path)) + Flowable.fromCallable(() -> new NetCopyConnectionInfo(path)) .flatMap( connectionInfo -> { Bundle retval = new Bundle(); @@ -1994,7 +1999,17 @@ public void showSftpDialog(String name, String path, boolean edit) { retval.putString(ARG_ADDRESS, connectionInfo.getHost()); retval.putInt(ARG_PORT, connectionInfo.getPort()); if (!TextUtils.isEmpty(connectionInfo.getDefaultPath())) { - retval.putString(ARG_DEFAULT_PATH, connectionInfo.getDefaultPath()); + retval.putString( + ARG_DEFAULT_PATH, + ArraysKt.joinToString( + connectionInfo.getDefaultPath().split("/"), + "/", + "", + "", + -1, + "", + (Function1) + s -> GenericExtKt.urlDecoded(s, Charsets.UTF_8))); } retval.putString(ARG_USERNAME, connectionInfo.getUsername()); @@ -2042,10 +2057,9 @@ public void addConnection( if (!edit) { if ((dataUtils.containsServer(path)) == -1) { Completable.fromRunnable( - () -> { - utilsHandler.saveToDatabase( - new OperationData(UtilsHandler.Operation.SMB, name, encryptedPath)); - }) + () -> + utilsHandler.saveToDatabase( + new OperationData(UtilsHandler.Operation.SMB, name, encryptedPath))) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( diff --git a/app/src/main/java/com/amaze/filemanager/utils/GenericExt.kt b/app/src/main/java/com/amaze/filemanager/utils/GenericExt.kt index 42d655d5e0..81ab58b4a4 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/GenericExt.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/GenericExt.kt @@ -20,6 +20,10 @@ package com.amaze.filemanager.utils +import java.net.URLDecoder.decode +import java.net.URLEncoder.encode +import java.nio.charset.Charset + /** * Allow null checks on more than one parameters at the same time. * Alternative of doing nested p1?.let p2?.let @@ -93,6 +97,24 @@ fun List<*>.containsPath(path: String): Boolean { (path.endsWith('/') && this.contains(path.substringBeforeLast('/'))) } +/** + * Convenience method to return a string in URL encoded form, with specified [Charset]. + * + * @param charset [Charset] to encode string. Default is UTF-8 + */ +fun String.urlEncoded(charset: Charset = Charsets.UTF_8): String { + return encode(this, charset.name()) +} + +/** + * Convenience method to return a string in URL decoded form, with specified [Charset]. + * + * @param charset [Charset] to decode string. Default is UTF-8 + */ +fun String.urlDecoded(charset: Charset = Charsets.UTF_8): String { + return decode(this, charset.name()) +} + interface Function { fun apply(t: T): R } diff --git a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java index 85996942b3..53a029c69e 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java @@ -731,24 +731,16 @@ public String parseCloudPath(OpenMode serviceType, String path) { switch (serviceType) { case DROPBOX: if (path.contains(CloudHandler.CLOUD_PREFIX_DROPBOX)) return path; - else - return CloudHandler.CLOUD_PREFIX_DROPBOX - + path.substring(path.indexOf(":") + 1, path.length()); + else return CloudHandler.CLOUD_PREFIX_DROPBOX + path.substring(path.indexOf(":") + 1); case BOX: if (path.contains(CloudHandler.CLOUD_PREFIX_BOX)) return path; - else - return CloudHandler.CLOUD_PREFIX_BOX - + path.substring(path.indexOf(":") + 1, path.length()); + else return CloudHandler.CLOUD_PREFIX_BOX + path.substring(path.indexOf(":") + 1); case GDRIVE: if (path.contains(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE)) return path; - else - return CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE - + path.substring(path.indexOf(":") + 1, path.length()); + else return CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE + path.substring(path.indexOf(":") + 1); case ONEDRIVE: if (path.contains(CloudHandler.CLOUD_PREFIX_ONE_DRIVE)) return path; - else - return CloudHandler.CLOUD_PREFIX_ONE_DRIVE - + path.substring(path.indexOf(":") + 1, path.length()); + else return CloudHandler.CLOUD_PREFIX_ONE_DRIVE + path.substring(path.indexOf(":") + 1); default: return path; } diff --git a/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt index 88c3e28eb7..72c8bfef1d 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt @@ -26,6 +26,9 @@ import android.text.TextUtils import android.util.Log import com.amaze.filemanager.fileoperations.filesystem.DOESNT_EXIST import com.amaze.filemanager.fileoperations.filesystem.WRITABLE_ON_REMOTE +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.AT +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLON import com.amaze.filemanager.filesystem.smb.CifsContexts.createWithDisableIpcSigningCheck import io.reactivex.Single import io.reactivex.schedulers.Schedulers @@ -48,42 +51,42 @@ object SmbUtil { /** Parse path to decrypt smb password */ @JvmStatic fun getSmbDecryptedPath(context: Context, path: String): String { - if (!(path.contains(":") && path.contains("@"))) { - // smb path doesn't have any credentials - return path - } - val buffer = StringBuilder() - val protocolPrefix = path.substringBefore("//") - val usernamePasswordSeparatorPos = path.indexOf(":", protocolPrefix.length) + 1 - buffer.append(path.substring(0, usernamePasswordSeparatorPos)) - val encryptedPassword = path.substring(usernamePasswordSeparatorPos, path.lastIndexOf("@")) - if (!TextUtils.isEmpty(encryptedPassword)) { - val decryptedPassword = PasswordUtil.decryptPassword(context, encryptedPassword) - buffer.append(decryptedPassword) - } - buffer.append(path.substring(path.lastIndexOf("@"))) - return buffer.toString() + return buildPath(path, withPassword = { + PasswordUtil.decryptPassword(context, it) + }) } /** Parse path to encrypt smb password */ @JvmStatic fun getSmbEncryptedPath(context: Context, path: String): String { - if (!(path.contains(":") && path.contains("@"))) { + return buildPath(path, withPassword = { + PasswordUtil.encryptPassword(context, it) + }) + } + + private fun buildPath(path: String, withPassword: (String) -> String?): String { + if (!(path.contains(COLON) && path.contains(AT))) { // smb path doesn't have any credentials return path } val buffer = StringBuilder() - // From index zero to user name including colon - val protocolPrefix = path.substringBefore("//") - val usernamePasswordSeparatorPos = path.indexOf(":", protocolPrefix.length) + 1 - buffer.append(path.substring(0, usernamePasswordSeparatorPos)) - val decryptedPassword = path.substring(usernamePasswordSeparatorPos, path.lastIndexOf("@")) - if (!TextUtils.isEmpty(decryptedPassword)) { - val encryptPassword = PasswordUtil.encryptPassword(context, decryptedPassword) - buffer.append(encryptPassword) + NetCopyConnectionInfo(path).let { connectionInfo -> + buffer.append(connectionInfo.prefix).append( + if (connectionInfo.username.isEmpty()) "" else connectionInfo.username.urlEncoded() + ) + if (false == connectionInfo.password?.isEmpty()) { + val password = withPassword.invoke(connectionInfo.password) + buffer.append(COLON).append(password) + } + buffer.append(AT).append(connectionInfo.host) + if (connectionInfo.port > 0) { + buffer.append(COLON).append(connectionInfo.port) + } + connectionInfo.defaultPath?.apply { + buffer.append(this) + } } - buffer.append(path.substring(path.lastIndexOf("@"))) - return buffer.toString() + return buffer.toString().replace("\n", "") } /** diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/MoveFilesTest.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/MoveFilesTest.java deleted file mode 100644 index 0e9a899d95..0000000000 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/MoveFilesTest.java +++ /dev/null @@ -1,102 +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.asynchronous.asynctasks; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Paths; -import java.security.SecureRandom; -import java.util.ArrayList; - -import org.apache.commons.compress.utils.IOUtils; -import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; - -import com.amaze.filemanager.asynchronous.asynctasks.movecopy.MoveFiles; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.filesystem.HybridFileParcelable; -import com.amaze.filemanager.filesystem.ssh.AbstractSftpServerTest; -import com.amaze.filemanager.shadows.ShadowMultiDex; - -import android.os.Environment; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -@RunWith(AndroidJUnit4.class) -@Config(shadows = {ShadowMultiDex.class}) -public class MoveFilesTest extends AbstractSftpServerTest { - - private File sshServerRoot; - - @Before - @Override - public void setUp() throws IOException { - sshServerRoot = Environment.getExternalStoragePublicDirectory("sshserver-test"); - serverPort = - createSshServer( - new VirtualFileSystemFactory(Paths.get(sshServerRoot.getAbsolutePath())), 64000); - prepareSshConnection(); - } - - @Test - @Ignore("Not yet for CI testing until pass") - public void testMoveFilesUsingSftp() throws Exception { - File sourceFile = new File(sshServerRoot, "testfile.bin"); - SecureRandom rnd = new SecureRandom(); - byte[] randomBytes = new byte[32]; - rnd.nextBytes(randomBytes); - IOUtils.copy(new ByteArrayInputStream(randomBytes), new FileOutputStream(sourceFile)); - - ArrayList filesToCopy = new ArrayList<>(); - HybridFileParcelable file = - new HybridFileParcelable( - "ssh://testuser:testpassword@127.0.0.1:" + serverPort + "/testfile.bin"); - file.generateMode(ApplicationProvider.getApplicationContext()); - filesToCopy.add(file); - ArrayList> filesToCopyPerFolder = new ArrayList<>(); - filesToCopyPerFolder.add(filesToCopy); - - ArrayList paths = new ArrayList<>(); - paths.add(Environment.getExternalStorageDirectory().getAbsolutePath()); - MoveFiles task = - new MoveFiles( - filesToCopyPerFolder, - false, - ApplicationProvider.getApplicationContext(), - OpenMode.FILE, - paths); - - task.call(); - - assertFalse(sourceFile.exists()); - assertTrue(new File(Environment.getExternalStorageDirectory(), "testfile.bin").exists()); - } -} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt index 491b7dd717..6225c84705 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh import android.content.Context -import android.os.Build +import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt index e94a850483..85fc4c54a0 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt @@ -36,13 +36,11 @@ import com.amaze.filemanager.database.UtilitiesDatabase.Companion.COLUMN_PRIVATE import com.amaze.filemanager.database.UtilitiesDatabase.Companion.COLUMN_PRIVATE_KEY_NAME import com.amaze.filemanager.database.UtilitiesDatabase.Companion.TABLE_SFTP import com.amaze.filemanager.database.UtilitiesDatabase.Companion.TABLE_SMB -import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowPasswordUtil import com.amaze.filemanager.utils.PasswordUtil import com.amaze.filemanager.utils.SmbUtil import org.junit.Assert.assertEquals -import org.junit.Assert.fail import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -153,28 +151,28 @@ class UtilitiesDatabaseMigrationTest { ) ) } - smbEntries.find { it.name == "test anonymous" }?.run { - assertEquals( - "smb://127.0.0.1/Public", - SmbUtil.getSmbDecryptedPath( - InstrumentationRegistry.getInstrumentation().targetContext, - this.path - ) - ) - } - val sftpEntries = utilitiesDatabase.sftpEntryDao().list().blockingGet() - sftpEntries.find { it.name == "test password" }?.run { - assertEquals( - "ssh://user:\\password/%&*()@10.0.0.1", - NetCopyClientUtils.decryptFtpPathAsNecessary(this.path) - ) - } ?: fail("test password entry not found") - sftpEntries.find { it.name == "test no password" }?.run { - assertEquals( - "ssh://user@10.0.0.2", - NetCopyClientUtils.decryptFtpPathAsNecessary(this.path) - ) - } ?: fail("test no password entry not found") +// smbEntries.find { it.name == "test anonymous" }?.run { +// assertEquals( +// "smb://127.0.0.1/Public", +// SmbUtil.getSmbDecryptedPath( +// InstrumentationRegistry.getInstrumentation().targetContext, +// this.path +// ) +// ) +// } +// val sftpEntries = utilitiesDatabase.sftpEntryDao().list().blockingGet() +// sftpEntries.find { it.name == "test password" }?.run { +// assertEquals( +// "ssh://user:\\password/%&*()@10.0.0.1", +// NetCopyClientUtils.decryptFtpPathAsNecessary(this.path) +// ) +// } ?: fail("test password entry not found") +// sftpEntries.find { it.name == "test no password" }?.run { +// assertEquals( +// "ssh://user@10.0.0.2", +// NetCopyClientUtils.decryptFtpPathAsNecessary(this.path) +// ) +// } ?: fail("test no password entry not found") utilitiesDatabase.close() } } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt index 2f3bc5223a..df2e40bf19 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt @@ -24,6 +24,7 @@ import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.getConnection import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.shutdown @@ -31,6 +32,7 @@ import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils.encryptFtpPathAsN import com.amaze.filemanager.filesystem.ssh.test.TestUtils import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowPasswordUtil +import com.amaze.filemanager.utils.PasswordUtil import io.reactivex.android.plugins.RxAndroidPlugins import io.reactivex.plugins.RxJavaPlugins import io.reactivex.schedulers.Schedulers @@ -53,6 +55,8 @@ import org.mockito.kotlin.verify import org.robolectric.annotation.Config import org.robolectric.shadows.ShadowSQLiteConnection import java.io.IOException +import java.net.URLEncoder.encode +import kotlin.text.Charsets.UTF_8 /** * Unit tests for [NetCopyClientConnectionPool] with FTP connections. @@ -86,7 +90,10 @@ class NetCopyClientConnectionPoolFtpTest { host = HOST, port = PORT, username = "testuser", - password = "testpassword" + password = PasswordUtil.encryptPassword( + AppConfig.getInstance(), + "testpassword" + ) ) ) assertNull( @@ -95,7 +102,10 @@ class NetCopyClientConnectionPoolFtpTest { host = HOST, port = PORT, username = "invaliduser", - password = "invalidpassword" + password = PasswordUtil.encryptPassword( + AppConfig.getInstance(), + "invalidpassword" + ) ) ) verify(mock, times(2)).connect(HOST, PORT) @@ -108,27 +118,9 @@ class NetCopyClientConnectionPoolFtpTest { */ @Test fun testGetConnectionWithUrl() { + val validUsername = "testuser" val validPassword = "testpassword" - val mock = createFTPClient("testuser", validPassword) - TestUtils.saveFtpConnectionSettings("testuser", validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://testuser:testpassword@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login("testuser", "testpassword") - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -137,27 +129,9 @@ class NetCopyClientConnectionPoolFtpTest { @Test @Throws(IOException::class) fun testGetConnectionWithUrlHavingComplexPassword1() { + val validUsername = "testuser" val validPassword = "testP@ssw0rd" - val mock = createFTPClient("testuser", validPassword) - TestUtils.saveFtpConnectionSettings("testuser", validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://testuser:testP@ssw0rd@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login("testuser", validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -165,112 +139,19 @@ class NetCopyClientConnectionPoolFtpTest { */ @Test fun testGetConnectionWithUrlHavingComplexPassword2() { + val validUsername = "testuser" val validPassword = "testP@##word" - val mock = createFTPClient("testuser", validPassword) - TestUtils.saveFtpConnectionSettings("testuser", validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://testuser:testP@##word@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login("testuser", validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") - } - - /** - * Test getting connection with URL/URI having complex credentials (case 1) - */ - @Test - fun testGetConnectionWithUrlHavingComplexCredential1() { - val validPassword = "testP@##word" - val mock = createFTPClient("testuser", validPassword) - TestUtils.saveFtpConnectionSettings("testuser", validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://testuser:testP@##word@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login("testuser", validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** - * Test getting connection with URL/URI having complex credentials (case 2) + * Test getting connection with URL/URI having complex password (case 3) */ @Test - fun testGetConnectionWithUrlHavingComplexCredential2() { - val validPassword = "testP@##word" - val mock = createFTPClient("testuser", validPassword) - TestUtils.saveFtpConnectionSettings("testuser", validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://testuser:testP@##word@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login("testuser", validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") - } - - /** - * Test getting connection with URL/URI having complex credentials (case 3) - */ - @Test - fun testGetConnectionWithUrlHavingComplexCredential3() { + fun testGetConnectionWithUrlHavingComplexPassword3() { val validUsername = "test@example.com" val validPassword = "testP@ssw0rd" - val mock = createFTPClient(validUsername, validPassword) - TestUtils.saveFtpConnectionSettings(validUsername, validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://test@example.com:testP@ssw0rd@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login(validUsername, validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -280,26 +161,7 @@ class NetCopyClientConnectionPoolFtpTest { fun testGetConnectionWithUrlHavingComplexCredential4() { val validUsername = "test@example.com" val validPassword = "testP@ssw0##$" - val mock = createFTPClient(validUsername, validPassword) - TestUtils.saveFtpConnectionSettings(validUsername, validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://test@example.com:testP@ssw0##$@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login(validUsername, validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -309,26 +171,7 @@ class NetCopyClientConnectionPoolFtpTest { fun testGetConnectionWithUrlHavingMinusSignInPassword1() { val validUsername = "test@example.com" val validPassword = "abcd-efgh" - val mock = createFTPClient(validUsername, validPassword) - TestUtils.saveFtpConnectionSettings(validUsername, validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://test@example.com:abcd-efgh@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login(validUsername, validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -338,26 +181,7 @@ class NetCopyClientConnectionPoolFtpTest { fun testGetConnectionWithUrlHavingMinusSignInPassword2() { val validUsername = "test@example.com" val validPassword = "---------------" - val mock = createFTPClient(validUsername, validPassword) - TestUtils.saveFtpConnectionSettings(validUsername, validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://test@example.com:---------------@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login(validUsername, validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -367,26 +191,7 @@ class NetCopyClientConnectionPoolFtpTest { fun testGetConnectionWithUrlHavingMinusSignInPassword3() { val validUsername = "test@example.com" val validPassword = "--agdiuhdpost15" - val mock = createFTPClient(validUsername, validPassword) - TestUtils.saveFtpConnectionSettings(validUsername, validPassword) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://test@example.com:--agdiuhdpost15@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).login(validUsername, validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -396,26 +201,32 @@ class NetCopyClientConnectionPoolFtpTest { fun testGetConnectionWithUrlHavingMinusSignInPassword4() { val validUsername = "test@example.com" val validPassword = "t-h-i-s-i-s-p-a-s-s-w-o-r-d-" + doRunTest(validUsername, validPassword) + } + + private fun doRunTest(validUsername: String, validPassword: String) { + val encodedUsername = encode(validUsername, UTF_8.name()) + val encodedPassword = encode(validPassword, UTF_8.name()) val mock = createFTPClient(validUsername, validPassword) TestUtils.saveFtpConnectionSettings(validUsername, validPassword) assertNotNull( getConnection( encryptFtpPathAsNecessary( - "ftp://test@example.com:t-h-i-s-i-s-p-a-s-s-w-o-r-d-@127.0.0.1:22222" + "ftp://$encodedUsername:$encodedPassword@127.0.0.1:22222" ) ) ) assertNull( getConnection( encryptFtpPathAsNecessary( - "ftp://invaliduser:invalidpassword@127.0.0.1:22222" + "ftp://$encodedInvalidUsername:$encodedInvalidPassword@127.0.0.1:22222" ) ) ) verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT verify(mock, atLeastOnce()).connect(HOST, PORT) verify(mock).login(validUsername, validPassword) - verify(mock, atMostOnce()).login("invaliduser", "invalidpassword") + verify(mock, atMostOnce()).login(invalidUsername, invalidPassword) } private fun createFTPClient(validUsername: String, validPassword: String): FTPClient { @@ -443,6 +254,10 @@ class NetCopyClientConnectionPoolFtpTest { const val HOST = "127.0.0.1" const val PORT = 22222 + private const val invalidUsername = "invaliduser" + private const val invalidPassword = "invalidpassword" + private val encodedInvalidUsername = encode(invalidUsername, UTF_8.name()) + private val encodedInvalidPassword = encode(invalidPassword, UTF_8.name()) /** * Bootstrap the unit test diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt index 83ad4dc7a9..d1c1b29eae 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt @@ -51,7 +51,7 @@ class NetCopyClientUtilTest { assertEquals( "/root/.config", NetCopyClientUtils.extractRemotePathFrom( - "ssh://root:a8/875dbc-==@127.0.0.1:9899/root/.config" + "ssh://root:YTgvODc1ZGJjLT09@127.0.0.1:9899/root/.config" ) ) assertEquals( diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt new file mode 100644 index 0000000000..1b141d0857 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2014-2022 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.filesystem.ftp + +import android.os.Build.VERSION_CODES.JELLY_BEAN +import android.os.Build.VERSION_CODES.KITKAT +import android.os.Build.VERSION_CODES.P +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.ShadowPasswordUtil +import com.amaze.filemanager.utils.urlDecoded +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.net.URLEncoder.encode + +/* ktlint-disable max-line-length */ +@RunWith(AndroidJUnit4::class) +@Config( + sdk = [JELLY_BEAN, KITKAT, P], + shadows = [ShadowPasswordUtil::class, ShadowMultiDex::class] +) +@Suppress("StringLiteralDuplication") +class NetCopyConnectionInfoTest { + + /** + * Test unsupported URL prefixes should throw IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException::class) + fun testUnsupportedPrefix() { + NetCopyConnectionInfo("http://github.com") + NetCopyConnectionInfo("smb://user:pass@127.0.0.1") + } + + /** + * Test parsing garbage. + */ + @Test(expected = IllegalArgumentException::class) + fun testParsingInvalid() { + NetCopyConnectionInfo("abcdefgh") + NetCopyConnectionInfo("svx[f//shf") + NetCopyConnectionInfo("ftp//abcde@12345@127.0.0.1") + } + + /** + * Tests simple parsing. + */ + @Test + fun testSimple() { + NetCopyConnectionInfo( + "ftp://testuser:testpassword@127.0.0.1:22222" + ).run { + assertEquals("ftp://", prefix) + assertEquals("testuser", username) + assertEquals("testpassword", password) + assertEquals("127.0.0.1", host) + assertEquals(22222, port) + assertNull(defaultPath) + assertNull(queryString) + assertNull(arguments) + assertNull(lastPathSegment()) + } + } + + /** + * Test parse URL with query string. + */ + @Test + fun testQueryString() { + NetCopyConnectionInfo( + "ftps://testuser:testpassword@127.0.0.1:22222?tls=implicit&passive=false" + ).run { + assertEquals("ftps://", prefix) + assertEquals("testuser", username) + assertEquals("testpassword", password) + assertEquals("127.0.0.1", host) + assertEquals(22222, port) + assertNull(defaultPath) + assertNull(lastPathSegment()) + assertEquals("tls=implicit&passive=false", queryString) + assertNotNull(arguments) + arguments?.run { + assertEquals("implicit", this["tls"]) + assertEquals("false", this["passive"]) + } + } + } + + /** + * Test default path in URL. + */ + @Test + fun testDefaultPath() { + NetCopyConnectionInfo( + "ftps://testuser:testpassword@127.0.0.1:22222/srv/tomopet-service" + ).run { + assertEquals("ftps://", prefix) + assertEquals("testuser", username) + assertEquals("testpassword", password) + assertEquals("127.0.0.1", host) + assertEquals(22222, port) + assertEquals("/srv/tomopet-service", defaultPath) + assertEquals("tomopet-service", lastPathSegment()) + assertNull(queryString) + assertNull(arguments) + } + } + + /** + * Test default path in URL. + */ + @Test + fun testDefaultPathWithFilename() { + NetCopyConnectionInfo( + "ftps://testuser:testpassword@127.0.0.1:22222/srv/tomopet-service/history.txt" + ).run { + assertEquals("ftps://", prefix) + assertEquals("testuser", username) + assertEquals("testpassword", password) + assertEquals("127.0.0.1", host) + assertEquals(22222, port) + assertEquals("/srv/tomopet-service", defaultPath) + assertEquals("history.txt", lastPathSegment()) + assertNull(queryString) + assertNull(arguments) + } + } + + /** + * Test default path and query string in URL. + */ + @Test + fun testDefaultPathWithQueryString() { + NetCopyConnectionInfo( + "ftps://testuser:testpassword@127.0.0.1:22222/srv/tomopet-service?tls=explicit&passive=true" + ).run { + assertEquals("ftps://", prefix) + assertEquals("testuser", username) + assertEquals("testpassword", password) + assertEquals("127.0.0.1", host) + assertEquals(22222, port) + assertEquals("/srv/tomopet-service", defaultPath) + assertEquals("tomopet-service", lastPathSegment()) + assertEquals("tls=explicit&passive=true", queryString) + assertNotNull(arguments) + arguments?.run { + assertEquals("explicit", this["tls"]) + assertEquals("true", this["passive"]) + } + } + } + + /** + * Test default path in URL. + */ + @Test + fun testDefaultPathURLEncoded() { + NetCopyConnectionInfo( + "ftps://testuser:testpassword@127.0.0.1:22222/Users/TranceLove/My+Documents/%40TranceLove%231433%261434" + ).run { + assertEquals("ftps://", prefix) + assertEquals("testuser", username) + assertEquals("testpassword", password) + assertEquals("127.0.0.1", host) + assertEquals(22222, port) + assertEquals("/Users/TranceLove/My+Documents/%40TranceLove%231433%261434", defaultPath) + assertEquals( + "/Users/TranceLove/My Documents/@TranceLove#1433&1434", + defaultPath?.urlDecoded() + ) + assertEquals("%40TranceLove%231433%261434", lastPathSegment()) + assertEquals("@TranceLove#1433&1434", lastPathSegment()?.urlDecoded()) + assertNull(queryString) + assertNull(arguments) + } + } + + /** + * Test default path in URL. + */ + @Test + fun testDefaultPathWithFilenameURLEncoded() { + NetCopyConnectionInfo( + "ftps://testuser:testpassword@127.0.0.1:22222/home/trancelove/My+Web+Sites/Test/Awesome-stars/%7BMaruell+Horbis%7D+Tris%2BSurplus+40%25+off+%40rugio.txt" + ).run { + assertEquals("ftps://", prefix) + assertEquals("testuser", username) + assertEquals("testpassword", password) + assertEquals("127.0.0.1", host) + assertEquals(22222, port) + assertEquals("/home/trancelove/My+Web+Sites/Test/Awesome-stars", defaultPath) + assertEquals( + "/home/trancelove/My Web Sites/Test/Awesome-stars", + defaultPath?.urlDecoded() + ) + assertEquals( + "%7BMaruell+Horbis%7D+Tris%2BSurplus+40%25+off+%40rugio.txt", + lastPathSegment() + ) + assertEquals( + "{Maruell Horbis} Tris+Surplus 40% off @rugio.txt", + lastPathSegment()?.urlDecoded() + ) + assertNull(queryString) + assertNull(arguments) + } + } + + /** + * Test default path and query string in URL. + */ + @Test + fun testDefaultPathWithQueryStringURLEncoded() { + NetCopyConnectionInfo( + "ftps://testuser:testpassword@127.0.0.1:22222/home/trancelove/My+Web+Sites/Test/Awesome-stars/%7BMaruell+Horbis%7D+Tris%2BSurplus+40%25+off+%40rugio.txt?easter_egg=%7B%7D%28%29%26%5E%25*%3C%3E%21%40%23%24%25%3F%3A%22%3B%27" + ).run { + assertEquals("ftps://", prefix) + assertEquals("testuser", username) + assertEquals("testpassword", password) + assertEquals("127.0.0.1", host) + assertEquals(22222, port) + assertEquals("/home/trancelove/My+Web+Sites/Test/Awesome-stars", defaultPath) + assertEquals( + "/home/trancelove/My Web Sites/Test/Awesome-stars", + defaultPath?.urlDecoded() + ) + assertEquals( + "%7BMaruell+Horbis%7D+Tris%2BSurplus+40%25+off+%40rugio.txt", + lastPathSegment() + ) + assertEquals( + "{Maruell Horbis} Tris+Surplus 40% off @rugio.txt", + lastPathSegment()?.urlDecoded() + ) + assertEquals( + "easter_egg=%7B%7D%28%29%26%5E%25*%3C%3E%21%40%23%24%25%3F%3A%22%3B%27", + queryString + ) + assertNotNull(arguments) + arguments?.run { + assertEquals( + "%7B%7D%28%29%26%5E%25*%3C%3E%21%40%23%24%25%3F%3A%22%3B%27", + this["easter_egg"] + ) + assertEquals("{}()&^%*<>!@#\$%?:\";'", this["easter_egg"]?.urlDecoded()) + } + } + } + + /** + * Tests difficult credentials. + */ + @Test + fun testDifficultCredentials() { + NetCopyConnectionInfo( + "ftp://testuser:${encode("testP@##word", Charsets.UTF_8.name())}@127.0.0.1:22222" + ).run { + assertEquals("ftp://", prefix) + assertEquals("testuser", username) + assertEquals("testP@##word", password) + assertEquals("127.0.0.1", host) + assertEquals(22222, port) + assertNull(defaultPath) + assertNull(queryString) + } + } + + /** + * Test parsing complex credentials. + */ + @Test + fun testComplexCredentials() { + val username = "user2816@user.com" + val password = "#$%^&*()10-={}" + val _username = encode(username, Charsets.UTF_8.name()) + val _password = encode(password, Charsets.UTF_8.name()) + + NetCopyConnectionInfo("ssh://$_username:$_password@127.0.0.1:32").run { + assertEquals("ssh://", this.prefix) + assertEquals("127.0.0.1", this.host) + assertEquals(32, this.port) + assertEquals(username, this.username) + assertEquals(password, this.password) + } + } +} +/* ktlint-disable max-line-length */ diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java deleted file mode 100644 index cf2ddf36ec..0000000000 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.java +++ /dev/null @@ -1,127 +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.filesystem.ssh; - -import static android.os.Build.VERSION_CODES.KITKAT; -import static android.os.Build.VERSION_CODES.P; -import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX; - -import java.io.IOException; -import java.net.BindException; -import java.nio.file.Paths; -import java.util.Collections; - -import org.apache.sshd.common.config.keys.KeyUtils; -import org.apache.sshd.common.file.FileSystemFactory; -import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; -import org.apache.sshd.server.SshServer; -import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator; -import org.apache.sshd.server.scp.ScpCommandFactory; -import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; - -import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool; -import com.amaze.filemanager.filesystem.ssh.test.TestKeyProvider; -import com.amaze.filemanager.shadows.ShadowMultiDex; - -import android.os.Build; -import android.os.Environment; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import io.reactivex.android.plugins.RxAndroidPlugins; -import io.reactivex.plugins.RxJavaPlugins; -import io.reactivex.schedulers.Schedulers; - -@RunWith(AndroidJUnit4.class) -@Config( - shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P, Build.VERSION_CODES.R}) -public abstract class AbstractSftpServerTest { - - protected SshServer server; - - protected static TestKeyProvider hostKeyProvider; - - protected int serverPort; - - @BeforeClass - public static void bootstrap() throws Exception { - hostKeyProvider = new TestKeyProvider(); - - RxJavaPlugins.reset(); - RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline()); - RxAndroidPlugins.reset(); - RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline()); - } - - @Before - public void setUp() throws IOException { - serverPort = - createSshServer( - new VirtualFileSystemFactory( - Paths.get(Environment.getExternalStorageDirectory().getAbsolutePath())), - 64000); - prepareSshConnection(); - } - - @After - public void tearDown() throws IOException { - NetCopyClientConnectionPool.INSTANCE.shutdown(); - if (server != null && server.isOpen()) { - server.stop(true); - } - } - - protected final void prepareSshConnection() { - String hostFingerprint = KeyUtils.getFingerPrint(hostKeyProvider.getKeyPair().getPublic()); - NetCopyClientConnectionPool.INSTANCE.getConnection( - SSH_URI_PREFIX, "127.0.0.1", serverPort, hostFingerprint, "testuser", "testpassword", null); - } - - protected final int createSshServer(FileSystemFactory fileSystemFactory, int startPort) - throws IOException { - - server = SshServer.setUpDefaultServer(); - - server.setFileSystemFactory(fileSystemFactory); - server.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE); - server.setHost("127.0.0.1"); - server.setKeyPairProvider(hostKeyProvider); - server.setCommandFactory(new ScpCommandFactory()); - server.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); - server.setPasswordAuthenticator( - ((username, password, session) -> - username.equals("testuser") && password.equals("testpassword"))); - - try { - server.setPort(startPort); - server.start(); - return startPort; - } catch (BindException ifPortIsUnavailable) { - return createSshServer(fileSystemFactory, startPort + 1); - } - } -} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt new file mode 100644 index 0000000000..aa5ca551b7 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt @@ -0,0 +1,162 @@ +/* + * 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.filesystem.ssh + +import android.os.Build.VERSION_CODES.JELLY_BEAN +import android.os.Build.VERSION_CODES.KITKAT +import android.os.Build.VERSION_CODES.P +import android.os.Environment +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.getConnection +import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.shutdown +import com.amaze.filemanager.filesystem.ssh.test.TestKeyProvider +import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.ShadowPasswordUtil +import com.amaze.filemanager.utils.PasswordUtil +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import org.apache.sshd.common.NamedFactory +import org.apache.sshd.common.config.keys.KeyUtils +import org.apache.sshd.common.file.FileSystemFactory +import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory +import org.apache.sshd.server.Command +import org.apache.sshd.server.SshServer +import org.apache.sshd.server.auth.password.PasswordAuthenticator +import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator +import org.apache.sshd.server.scp.ScpCommandFactory +import org.apache.sshd.server.session.ServerSession +import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.io.IOException +import java.net.BindException +import java.nio.file.Paths + +/** + * Base class for all SSH server related tests. + */ +@RunWith(AndroidJUnit4::class) +@Config( + shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], + sdk = [JELLY_BEAN, KITKAT, P] +) +abstract class AbstractSftpServerTest { + + protected var encryptedPassword: String? = + PasswordUtil.encryptPassword(AppConfig.getInstance(), PASSWORD) + protected var serverPort = 0 + private lateinit var server: SshServer + + /** + * Setup SSH server with device directory as root. + */ + @Before + @Throws(IOException::class) + open fun setUp() { + serverPort = createSshServer( + VirtualFileSystemFactory( + Paths.get(Environment.getExternalStorageDirectory().absolutePath) + ), + 64000 + ) + prepareSshConnection() + } + + /** + * Shutdown SSH server after test. + */ + @After + @Throws(IOException::class) + fun tearDown() { + shutdown() + if (server.isOpen) { + server.stop(true) + } + } + + protected fun prepareSshConnection() { + val hostFingerprint = KeyUtils.getFingerPrint(hostKeyProvider.keyPair.public) + getConnection( + SSH_URI_PREFIX, + HOST, + serverPort, + hostFingerprint, + USERNAME, + PasswordUtil.encryptPassword(AppConfig.getInstance(), PASSWORD), + null + ) + } + + @Throws(IOException::class) + protected fun createSshServer(fileSystemFactory: FileSystemFactory, startPort: Int): Int { + server = SshServer.setUpDefaultServer() + server.fileSystemFactory = fileSystemFactory + server.publickeyAuthenticator = AcceptAllPublickeyAuthenticator.INSTANCE + server.host = HOST + server.keyPairProvider = hostKeyProvider + server.commandFactory = ScpCommandFactory() + server.subsystemFactories = listOf>(SftpSubsystemFactory()) + server.passwordAuthenticator = + PasswordAuthenticator { username: String, password: String, _: ServerSession? -> + username == USERNAME && password == PASSWORD + } + return try { + server.port = startPort + server.start() + startPort + } catch (ifPortIsUnavailable: BindException) { + createSshServer(fileSystemFactory, startPort + 1) + } + } + + companion object { + + @JvmStatic + protected val HOST = "127.0.0.1" + + @JvmStatic + protected val USERNAME = "testuser" + + @JvmStatic + protected val PASSWORD = "testpassword" + + protected lateinit var hostKeyProvider: TestKeyProvider + + /** + * Prepare SSH host key provider and RxJava scheduler handlers. + */ + @BeforeClass + @JvmStatic + fun bootstrap() { + hostKeyProvider = TestKeyProvider() + RxJavaPlugins.reset() + RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.reset() + RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } + } + } +} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/CreateFileOnSshdTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/CreateFileOnSshdTest.java deleted file mode 100644 index 3bffa7e7e3..0000000000 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/CreateFileOnSshdTest.java +++ /dev/null @@ -1,64 +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.filesystem.ssh; - -import java.io.IOException; -import java.nio.file.FileSystem; -import java.nio.file.Paths; -import java.util.Collections; - -import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; -import org.apache.sshd.common.session.Session; -import org.junit.Ignore; -import org.junit.Test; - -import com.amaze.filemanager.filesystem.ssh.test.BlockFileCreationFileSystemProvider; - -import android.os.Environment; - -@Ignore("Skipped due to no solid test case given") -public class CreateFileOnSshdTest extends AbstractSftpServerTest { - - @Test - public void testCreateFileNormal() throws Exception { - tearDown(); - createSshServer( - new VirtualFileSystemFactory( - Paths.get(Environment.getExternalStorageDirectory().getAbsolutePath())), - serverPort); - } - - @Test - public void testCreateFilePermissionDenied() throws Exception { - tearDown(); - createSshServer( - new VirtualFileSystemFactory() { - @Override - public FileSystem createFileSystem(Session session) throws IOException { - return new BlockFileCreationFileSystemProvider() - .newFileSystem( - Paths.get(Environment.getExternalStorageDirectory().getAbsolutePath()), - Collections.emptyMap()); - } - }, - serverPort); - } -} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.java deleted file mode 100644 index db4208ed52..0000000000 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.java +++ /dev/null @@ -1,183 +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.filesystem.ssh; - -import static org.awaitility.Awaitility.await; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasItems; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Test; - -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.filesystem.HybridFile; - -import android.os.Environment; - -import androidx.test.core.app.ApplicationProvider; - -public class ListFilesOnSshdTest extends AbstractSftpServerTest { - - @Test - public void testNormalListDirs() { - for (String s : new String[] {"sysroot", "srv", "var", "tmp", "bin", "lib", "usr"}) { - new File(Environment.getExternalStorageDirectory(), s).mkdir(); - } - assertTrue(performVerify()); - } - - @Test - public void testListDirsAndSymlinks() throws Exception { - createNecessaryDirsForSymlinkRelatedTests(); - assertTrue(performVerify()); - } - - private void createNecessaryDirsForSymlinkRelatedTests() throws IOException { - File sysroot = new File(Environment.getExternalStorageDirectory(), "sysroot"); - sysroot.mkdir(); - for (String s : new String[] {"srv", "var", "tmp"}) { - File subdir = new File(sysroot, s); - subdir.mkdir(); - Files.createSymbolicLink( - Paths.get(new File(Environment.getExternalStorageDirectory(), s).getAbsolutePath()), - Paths.get(subdir.getAbsolutePath())); - } - for (String s : new String[] {"bin", "lib", "usr"}) { - new File(Environment.getExternalStorageDirectory(), s).mkdir(); - } - } - - private boolean performVerify() { - List result = new ArrayList<>(); - HybridFile file = - new HybridFile(OpenMode.SFTP, "ssh://testuser:testpassword@127.0.0.1:" + serverPort); - file.forEachChildrenFile( - ApplicationProvider.getApplicationContext(), - false, - (fileFound) -> { - assertTrue(fileFound.getPath() + " not seen as directory", fileFound.isDirectory()); - result.add(fileFound.getName()); - }); - await().until(() -> result.size() == 7); - assertThat(result, hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr")); - return true; - } - - @Test - public void testListDirsAndFilesAndSymlinks() throws Exception { - createNecessaryDirsForSymlinkRelatedTests(); - for (int i = 1; i <= 4; i++) { - File f = new File(Environment.getExternalStorageDirectory(), i + ".txt"); - FileOutputStream out = new FileOutputStream(f); - out.write(i); - out.close(); - Files.createSymbolicLink( - Paths.get( - new File(Environment.getExternalStorageDirectory(), "symlink" + i + ".txt") - .getAbsolutePath()), - Paths.get(f.getAbsolutePath())); - } - List dirs = new ArrayList<>(), files = new ArrayList<>(); - HybridFile file = - new HybridFile(OpenMode.SFTP, "ssh://testuser:testpassword@127.0.0.1:" + serverPort); - file.forEachChildrenFile( - ApplicationProvider.getApplicationContext(), - false, - (fileFound) -> { - if (!fileFound.getName().endsWith(".txt")) { - assertTrue(fileFound.getPath() + " not seen as directory", fileFound.isDirectory()); - dirs.add(fileFound.getName()); - } else { - assertFalse(fileFound.getPath() + " not seen as file", fileFound.isDirectory()); - files.add(fileFound.getName()); - } - }); - await().until(() -> dirs.size() == 7); - assertThat(dirs, hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr")); - assertThat( - files, - hasItems( - "1.txt", - "2.txt", - "3.txt", - "4.txt", - "symlink1.txt", - "symlink2.txt", - "symlink3.txt", - "symlink4.txt")); - } - - @Test - public void testListDirsAndBrokenSymlinks() throws Exception { - createNecessaryDirsForSymlinkRelatedTests(); - Files.createSymbolicLink( - Paths.get( - new File(Environment.getExternalStorageDirectory(), "b0rken.symlink") - .getAbsolutePath()), - Paths.get(new File("/tmp/notfound.file").getAbsolutePath())); - assertTrue(performVerify()); - } - - @Test - public void testListDirsWithDirectPathToDir() throws Exception { - createNecessaryDirsForSymlinkRelatedTests(); - for (int i = 1; i <= 4; i++) { - File f = new File(new File(Environment.getExternalStorageDirectory(), "tmp"), i + ".txt"); - FileOutputStream out = new FileOutputStream(f); - out.write(i); - out.close(); - } - List result = new ArrayList<>(); - HybridFile file = - new HybridFile( - OpenMode.SFTP, "ssh://testuser:testpassword@127.0.0.1:" + serverPort + "/tmp"); - file.forEachChildrenFile( - ApplicationProvider.getApplicationContext(), - false, - (fileFound) -> { - assertFalse(fileFound.getPath() + " not seen as file", fileFound.isDirectory()); - result.add(fileFound.getName()); - }); - await().until(() -> result.size() == 4); - assertThat(result, hasItems("1.txt", "2.txt", "3.txt", "4.txt")); - List result2 = new ArrayList<>(); - file = - new HybridFile(OpenMode.SFTP, file.getParent(ApplicationProvider.getApplicationContext())); - file.forEachChildrenFile( - ApplicationProvider.getApplicationContext(), - false, - (fileFound) -> { - assertTrue(fileFound.getPath() + " not seen as directory", fileFound.isDirectory()); - result2.add(fileFound.getName()); - }); - await().until(() -> result2.size() == 7); - assertThat(result2, hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr")); - } -} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.kt new file mode 100644 index 0000000000..41bacb8736 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.kt @@ -0,0 +1,245 @@ +/* + * 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.filesystem.ssh + +import android.os.Environment +import androidx.test.core.app.ApplicationProvider +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.HybridFile +import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.utils.OnFileFound +import org.awaitility.Awaitility.await +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths + +/** + * Test listing files on SSH server. + */ +@Suppress("StringLiteralDuplication") +class ListFilesOnSshdTest : AbstractSftpServerTest() { + + /** + * Test list directories normally + */ + @Test + fun testNormalListDirs() { + for (s in arrayOf("sysroot", "srv", "var", "tmp", "bin", "lib", "usr")) { + File(Environment.getExternalStorageDirectory(), s).mkdir() + } + assertTrue(performVerify()) + } + + /** + * Test list directories and symbolic links + */ + @Test + @Throws(Exception::class) + fun testListDirsAndSymlinks() { + createNecessaryDirsForSymlinkRelatedTests() + assertTrue(performVerify()) + } + + @Throws(IOException::class) + private fun createNecessaryDirsForSymlinkRelatedTests() { + val sysroot = File(Environment.getExternalStorageDirectory(), "sysroot") + sysroot.mkdir() + for (s in arrayOf("srv", "var", "tmp")) { + val subdir = File(sysroot, s) + subdir.mkdir() + Files.createSymbolicLink( + Paths.get(File(Environment.getExternalStorageDirectory(), s).absolutePath), + Paths.get(subdir.absolutePath) + ) + } + for (s in arrayOf("bin", "lib", "usr")) { + File(Environment.getExternalStorageDirectory(), s).mkdir() + } + } + + private fun performVerify(): Boolean { + val result: MutableList = ArrayList() + val file = HybridFile( + OpenMode.SFTP, + "ssh://$USERNAME:$encryptedPassword@$HOST:$serverPort" + ) + file.forEachChildrenFile( + ApplicationProvider.getApplicationContext(), + false, + object : OnFileFound { + override fun onFileFound(fileFound: HybridFileParcelable) { + assertTrue("${fileFound.path} not seen as directory", fileFound.isDirectory) + result.add(fileFound.name) + } + } + ) + await().until { result.size == 7 } + assertThat>( + result, + Matchers.hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr") + ) + return true + } + + /** + * Test list files, directories and symbolic links altogether. + */ + @Test + @Throws(Exception::class) + fun testListDirsAndFilesAndSymlinks() { + createNecessaryDirsForSymlinkRelatedTests() + for (i in 1..4) { + val f = File(Environment.getExternalStorageDirectory(), "$i.txt") + val out = FileOutputStream(f) + out.write(i) + out.close() + Files.createSymbolicLink( + Paths.get( + File(Environment.getExternalStorageDirectory(), "symlink$i.txt") + .absolutePath + ), + Paths.get(f.absolutePath) + ) + } + val dirs: MutableList = ArrayList() + val files: MutableList = ArrayList() + val file = HybridFile( + OpenMode.SFTP, + "ssh://$USERNAME:$encryptedPassword@$HOST:$serverPort" + ) + file.forEachChildrenFile( + ApplicationProvider.getApplicationContext(), + false, + object : OnFileFound { + override fun onFileFound(fileFound: HybridFileParcelable) { + if (!fileFound.name.endsWith(".txt")) { + assertTrue( + fileFound.path + " not seen as directory", + fileFound.isDirectory + ) + dirs.add(fileFound.name) + } else { + assertFalse( + fileFound.path + " not seen as file", + fileFound.isDirectory + ) + files.add(fileFound.name) + } + } + } + ) + await().until { dirs.size == 7 } + assertThat>( + dirs, + Matchers.hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr") + ) + assertThat>( + files, + Matchers.hasItems( + "1.txt", + "2.txt", + "3.txt", + "4.txt", + "symlink1.txt", + "symlink2.txt", + "symlink3.txt", + "symlink4.txt" + ) + ) + } + + /** + * Test list directories with broken symbolic links. + */ + @Test + @Throws(Exception::class) + fun testListDirsAndBrokenSymlinks() { + createNecessaryDirsForSymlinkRelatedTests() + Files.createSymbolicLink( + Paths.get( + File(Environment.getExternalStorageDirectory(), "b0rken.symlink") + .absolutePath + ), + Paths.get(File("/tmp/notfound.file").absolutePath) + ) + assertTrue(performVerify()) + } + + /** + * Test listing directories with full path to directory to list. + */ + @Test + @Throws(Exception::class) + fun testListDirsWithDirectPathToDir() { + createNecessaryDirsForSymlinkRelatedTests() + for (i in 1..4) { + val f = File(File(Environment.getExternalStorageDirectory(), "tmp"), "$i.txt") + val out = FileOutputStream(f) + out.write(i) + out.close() + } + val result: MutableList = ArrayList() + var file = HybridFile( + OpenMode.SFTP, + "ssh://$USERNAME:$encryptedPassword@$HOST:$serverPort/tmp" + ) + file.forEachChildrenFile( + ApplicationProvider.getApplicationContext(), + false, + object : OnFileFound { + override fun onFileFound(fileFound: HybridFileParcelable) { + assertFalse("${fileFound.path} not seen as directory", fileFound.isDirectory) + result.add(fileFound.name) + } + } + ) + await().until { result.size == 4 } + assertThat>( + result, + Matchers.hasItems("1.txt", "2.txt", "3.txt", "4.txt") + ) + val result2: MutableList = ArrayList() + file = + HybridFile(OpenMode.SFTP, file.getParent(ApplicationProvider.getApplicationContext())) + file.forEachChildrenFile( + ApplicationProvider.getApplicationContext(), + false, + object : OnFileFound { + override fun onFileFound(fileFound: HybridFileParcelable) { + assertTrue("${fileFound.path} not seen as directory", fileFound.isDirectory) + result2.add(fileFound.name) + } + } + ) + await().until { result2.size == 7 } + assertThat>( + result2, + Matchers.hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr") + ) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt index 4847947be9..285fed9b36 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt @@ -24,6 +24,7 @@ import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSHClientFactory import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX @@ -33,6 +34,7 @@ import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils.encryptFtpPathAsN import com.amaze.filemanager.filesystem.ssh.test.TestUtils import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowPasswordUtil +import com.amaze.filemanager.utils.PasswordUtil import com.amaze.filemanager.utils.Utils import io.reactivex.android.plugins.RxAndroidPlugins import io.reactivex.plugins.RxJavaPlugins @@ -61,7 +63,10 @@ import org.mockito.kotlin.verify import org.robolectric.annotation.Config import org.robolectric.shadows.ShadowSQLiteConnection import java.io.IOException +import java.net.URLEncoder.encode import java.security.KeyPair +import java.security.PrivateKey +import kotlin.text.Charsets.UTF_8 /** * Tests for [NetCopyClientConnectionPool] with SSH connections. @@ -97,7 +102,10 @@ class NetCopyClientConnectionPoolSshTest { PORT, SecurityUtils.getFingerprint(hostKeyPair.public), "testuser", - "testpassword", + PasswordUtil.encryptPassword( + AppConfig.getInstance(), + "testpassword" + ), null ) ) @@ -108,7 +116,10 @@ class NetCopyClientConnectionPoolSshTest { PORT, SecurityUtils.getFingerprint(hostKeyPair.public), "invaliduser", - "invalidpassword", + PasswordUtil.encryptPassword( + AppConfig.getInstance(), + "invalidpassword" + ), null ) ) @@ -170,40 +181,9 @@ class NetCopyClientConnectionPoolSshTest { @Test @Throws(IOException::class) fun testGetConnectionWithUrl() { + val validUsername = "testuser" val validPassword = "testpassword" - val mock = createSshServer("testuser", validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - "testuser", - validPassword, - null - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://testuser:testpassword@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword("testuser", "testpassword") - // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -213,51 +193,9 @@ class NetCopyClientConnectionPoolSshTest { @Test @Throws(IOException::class) fun testGetConnectionWithUrlHavingSubpath() { + val validUsername = "testuser" val validPassword = "testpassword" - val mock = createSshServer("testuser", validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - "testuser", - validPassword, - null, - "/home/testuser" - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://testuser:testpassword@127.0.0.1:22222/home/testuser" - ) - ) - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://testuser:testpassword@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222/home/testuser" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, Mockito.atLeastOnce()) - .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.public)) - verify(mock, Mockito.atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, Mockito.atLeastOnce()).connect("127.0.0.1", 22222) - verify(mock).authPassword("testuser", "testpassword") - // invalid username won't give host key. Should never called this - verify(mock, Mockito.never()).authPassword("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword, "/home/testuser") } /** @@ -266,27 +204,16 @@ class NetCopyClientConnectionPoolSshTest { @Test @Throws(IOException::class) fun testGetConnectionWithUrlAndKeyAuth() { - val mock = createSshServer("testuser", null) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - "testuser", - null, - userKeyPair.private - ) - assertNotNull(getConnection("ssh://testuser@127.0.0.1:22222")) - assertNull(getConnection("ssh://invaliduser@127.0.0.1:22222")) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPublickey("testuser", sshKeyProvider) - // invalid username won't give host key. Should never called this - verify(mock, never()).authPublickey("invaliduser", sshKeyProvider) + doRunTest(validUsername = "testuser", validPrivateKey = userKeyPair.private) + } + + /** + * Test getting connection with URL/URI using key authentication having complex username. + */ + @Test + @Throws(IOException::class) + fun testGetConnectionWithUrlAndKeyAuthHavingComplexUsername() { + doRunTest(validUsername = "test@example.com", validPrivateKey = userKeyPair.private) } /** @@ -296,27 +223,7 @@ class NetCopyClientConnectionPoolSshTest { @Test @Throws(IOException::class) fun testGetConnectionWithUrlAndKeyAuthHavingSubpath() { - val mock = createSshServer("testuser", null) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - "testuser", - null, - userKeyPair.private, - "/home/testuser" - ) - assertNotNull(getConnection("ssh://testuser@127.0.0.1:22222/home/testuser")) - assertNotNull(getConnection("ssh://testuser@127.0.0.1:22222")) - assertNull(getConnection("ssh://invaliduser@127.0.0.1:22222/home/testuser")) - assertNull(getConnection("ssh://invaliduser@127.0.0.1:22222")) - verify(mock, Mockito.atLeastOnce()) - .addHostKeyVerifier(SecurityUtils.getFingerprint(hostKeyPair.public)) - verify(mock, Mockito.atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, Mockito.atLeastOnce()).connect("127.0.0.1", 22222) - verify(mock).authPublickey("testuser", sshKeyProvider) - // invalid username won't give host key. Should never called this - verify(mock, Mockito.never()) - .authPublickey("invaliduser", sshKeyProvider) + doRunTest("testuser", userKeyPair.private, "/home/testuser") } /** @@ -325,82 +232,20 @@ class NetCopyClientConnectionPoolSshTest { @Test @Throws(IOException::class) fun testGetConnectionWithUrlHavingComplexPassword1() { + val validUsername = "testuser" val validPassword = "testP@ssw0rd" - val mock = createSshServer("testuser", validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - "testuser", - validPassword, - null - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://testuser:testP@ssw0rd@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword("testuser", validPassword) - // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** - * Test getting connection with URL/URI having complex password (case 2) + * Test getting connection with URL/URI having complex passwords (case 2) */ @Test @Throws(IOException::class) fun testGetConnectionWithUrlHavingComplexPassword2() { + val validUsername = "testuser" val validPassword = "testP@##word" - val mock = createSshServer("testuser", validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - "testuser", - validPassword, - null - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://testuser:testP@##word@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword("testuser", validPassword) - // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -409,168 +254,20 @@ class NetCopyClientConnectionPoolSshTest { @Test @Throws(IOException::class) fun testGetConnectionWithUrlHavingComplexCredential1() { - val validPassword = "testP@##word" - val mock = createSshServer("testuser", validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - "testuser", - validPassword, - null - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://testuser:testP@##word@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword("testuser", validPassword) - // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") - } - - /** - * Test getting connection with URL/URI having complex credentials (case 2) - */ - @Test - @Throws(IOException::class) - fun testGetConnectionWithUrlHavingComplexCredential2() { - val validPassword = "testP@##word" - val mock = createSshServer("testuser", validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - "testuser", - validPassword, - null - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://testuser:testP@##word@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword("testuser", validPassword) - // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") - } - - /** - * Test getting connection with URL/URI having complex credentials (case 3) - */ - @Test - @Throws(IOException::class) - fun testGetConnectionWithUrlHavingComplexCredential3() { val validUsername = "test@example.com" val validPassword = "testP@ssw0rd" - val mock = createSshServer(validUsername, validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - validUsername, - validPassword, - null - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://test@example.com:testP@ssw0rd@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword(validUsername, validPassword) - // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** - * Test getting connection with URL/URI having complex credentials (case 4) + * Test getting connection with URL/URI having complex credentials (case 2) */ @Test @Throws(IOException::class) - fun testGetConnectionWithUrlHavingComplexCredential4() { + fun testGetConnectionWithUrlHavingComplexCredential2() { val validUsername = "test@example.com" val validPassword = "testP@ssw0##$" - val mock = createSshServer(validUsername, validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - validUsername, - validPassword, - null - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://test@example.com:testP@ssw0##$@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword(validUsername, validPassword) - // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -581,39 +278,7 @@ class NetCopyClientConnectionPoolSshTest { fun testGetConnectionWithUrlHavingMinusSignInPassword1() { val validUsername = "test@example.com" val validPassword = "abcd-efgh" - val mock = createSshServer(validUsername, validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - validUsername, - validPassword, - null - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://test@example.com:abcd-efgh@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword(validUsername, validPassword) - // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -624,39 +289,7 @@ class NetCopyClientConnectionPoolSshTest { fun testGetConnectionWithUrlHavingMinusSignInPassword2() { val validUsername = "test@example.com" val validPassword = "---------------" - val mock = createSshServer(validUsername, validPassword) - TestUtils.saveSshConnectionSettings( - hostKeyPair, - validUsername, - validPassword, - null - ) - assertNotNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://test@example.com:---------------@127.0.0.1:22222" - ) - ) - ) - assertNull( - getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) - ) - ) - verify(mock, atLeastOnce()) - .addHostKeyVerifier( - SecurityUtils.getFingerprint( - hostKeyPair.public - ) - ) - verify(mock, atLeastOnce()).connectTimeout = - NetCopyClientConnectionPool.CONNECT_TIMEOUT - verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword(validUsername, validPassword) - // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") + doRunTest(validUsername, validPassword) } /** @@ -667,24 +300,50 @@ class NetCopyClientConnectionPoolSshTest { fun testGetConnectionWithUrlHavingMinusSignInPassword3() { val validUsername = "test@example.com" val validPassword = "--agdiuhdpost15" + doRunTest(validUsername, validPassword) + } + + /** + * Test getting connection with URL/URI having minus sign in password (case 4) + */ + @Test + @Throws(IOException::class) + fun testGetConnectionWithUrlHavingMinusSignInPassword4() { + val validUsername = "test@example.com" + val validPassword = "t-h-i-s-i-s-p-a-s-s-w-o-r-d-" + doRunTest(validUsername, validPassword) + } + + private fun doRunTest(validUsername: String, validPassword: String, subPath: String? = null) { + val encodedUsername = encode(validUsername, UTF_8.name()) + val encodedPassword = encode(validPassword, UTF_8.name()) val mock = createSshServer(validUsername, validPassword) TestUtils.saveSshConnectionSettings( hostKeyPair, validUsername, validPassword, - null + null, + subPath ) assertNotNull( getConnection( encryptFtpPathAsNecessary( - "ssh://test@example.com:--agdiuhdpost15@127.0.0.1:22222" + if (subPath.isNullOrEmpty()) { + "ssh://$encodedUsername:$encodedPassword@$HOST:$PORT" + } else { + "ssh://$encodedUsername:$encodedPassword@$HOST:$PORT$subPath" + } ) ) ) assertNull( getConnection( encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" + if (subPath.isNullOrEmpty()) { + "ssh://$encodedInvalidUsername:$encodedInvalidPassword@$HOST:$PORT" + } else { + "ssh://$encodedInvalidUsername:$encodedInvalidPassword@$HOST:$PORT$subPath" + } ) ) ) @@ -699,36 +358,39 @@ class NetCopyClientConnectionPoolSshTest { verify(mock, atLeastOnce()).connect(HOST, PORT) verify(mock).authPassword(validUsername, validPassword) // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") + verify(mock, never()).authPassword(invalidUsername, invalidPassword) } - /** - * Test getting connection with URL/URI having minus sign in password (case 4) - */ - @Test - @Throws(IOException::class) - fun testGetConnectionWithUrlHavingMinusSignInPassword4() { - val validUsername = "test@example.com" - val validPassword = "t-h-i-s-i-s-p-a-s-s-w-o-r-d-" - val mock = createSshServer(validUsername, validPassword) + private fun doRunTest( + validUsername: String, + validPrivateKey: PrivateKey = userKeyPair.private, + subPath: String? = null + ) { + val encodedUsername = encode(validUsername, UTF_8.name()) + val mock = createSshServer(validUsername, null) TestUtils.saveSshConnectionSettings( hostKeyPair, validUsername, - validPassword, - null + null, + validPrivateKey, + subPath ) assertNotNull( getConnection( - encryptFtpPathAsNecessary( - "ssh://test@example.com:t-h-i-s-i-s-p-a-s-s-w-o-r-d-@127.0.0.1:22222" - ) + if (subPath.isNullOrEmpty()) { + "ssh://$encodedUsername@$HOST:$PORT" + } else { + "ssh://$encodedUsername@$HOST:$PORT$subPath" + } ) ) assertNull( getConnection( - encryptFtpPathAsNecessary( - "ssh://invaliduser:invalidpassword@127.0.0.1:22222" - ) + if (subPath.isNullOrEmpty()) { + "ssh://$encodedInvalidUsername@$HOST:$PORT" + } else { + "ssh://$encodedInvalidUsername@$HOST:$PORT$subPath" + } ) ) verify(mock, atLeastOnce()) @@ -740,9 +402,9 @@ class NetCopyClientConnectionPoolSshTest { verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT verify(mock, atLeastOnce()).connect(HOST, PORT) - verify(mock).authPassword(validUsername, validPassword) + verify(mock).authPublickey(validUsername, sshKeyProvider) // invalid username won't give host key. Should never called this - verify(mock, never()).authPassword("invaliduser", "invalidpassword") + verify(mock, never()).authPublickey(invalidPassword, sshKeyProvider) } @Throws(IOException::class) @@ -789,6 +451,10 @@ class NetCopyClientConnectionPoolSshTest { const val HOST = "127.0.0.1" const val PORT = 22222 + private const val invalidUsername = "invaliduser" + private const val invalidPassword = "invalidpassword" + private val encodedInvalidUsername = encode(invalidUsername, UTF_8.name()) + private val encodedInvalidPassword = encode(invalidPassword, UTF_8.name()) lateinit var hostKeyPair: KeyPair lateinit var userKeyPair: KeyPair diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/test/TestUtils.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/test/TestUtils.kt index 8386ba6e5a..1bb2cc2b5f 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/test/TestUtils.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/test/TestUtils.kt @@ -35,10 +35,12 @@ import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.json.JSONObject import org.robolectric.Shadows import java.io.StringWriter +import java.net.URLEncoder.encode import java.security.KeyPair import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.SecureRandom +import kotlin.text.Charsets.UTF_8 /** * Test support util methods. @@ -72,8 +74,8 @@ object TestUtils { } ) if (validUsername != "" && validPassword != "") { - fullUri.append(validUsername) - fullUri.append(':').append(validPassword).append("@") + fullUri.append(encode(validUsername, UTF_8.name())) + fullUri.append(':').append(encode(validPassword, UTF_8.name())).append("@") } fullUri.append("${NetCopyClientConnectionPoolFtpTest.HOST}:$port") @@ -110,8 +112,9 @@ object TestUtils { jw.close() privateKeyContents = writer.toString() } - val fullUri: StringBuilder = StringBuilder().append(SSH_URI_PREFIX).append(validUsername) - if (validPassword != null) fullUri.append(':').append(validPassword) + val fullUri: StringBuilder = StringBuilder() + .append(SSH_URI_PREFIX).append(encode(validUsername, UTF_8.name())) + if (validPassword != null) fullUri.append(':').append(encode(validPassword, UTF_8.name())) fullUri.append( "@${NetCopyClientConnectionPoolSshTest.HOST}:${NetCopyClientConnectionPoolSshTest.PORT}" ) 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 443dfe4d6c..20f1b4aa2a 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 @@ -98,7 +98,7 @@ public class MainActivityTest { private static final String[] BUNDLE_KEYS = { - "address", "port", "keypairName", "name", "username", "password", "edit" + "address", "port", "keypairName", "name", "username", "password", "edit", "defaultPath" }; @Rule @@ -163,7 +163,60 @@ public void testInvokeSftpConnectionDialogWithPassword() verify.putString("username", "root"); verify.putBoolean("hasPassword", true); verify.putBoolean("edit", true); - verify.putString("password", "12345678"); + verify.putString( + "password", + PasswordUtil.INSTANCE + .encryptPassword(AppConfig.getInstance(), "12345678", Base64.URL_SAFE) + .replace("\n", "")); + + testOpenSftpConnectDialog(uri, verify); + } + + @Test + public void testInvokeSftpConnectionDialogWithPasswordAndDefaultPath() + throws GeneralSecurityException, IOException { + String uri = + NetCopyClientUtils.INSTANCE.encryptFtpPathAsNecessary( + "ssh://root:12345678@127.0.0.1:22/data/incoming"); + + 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("defaultPath", "/data/incoming"); + verify.putString( + "password", + PasswordUtil.INSTANCE + .encryptPassword(AppConfig.getInstance(), "12345678", Base64.URL_SAFE) + .replace("\n", "")); + + testOpenSftpConnectDialog(uri, verify); + } + + @Test + public void testInvokeSftpConnectionDialogWithPasswordAndEncodedDefaultPath() + throws GeneralSecurityException, IOException { + String uri = + NetCopyClientUtils.INSTANCE.encryptFtpPathAsNecessary( + "ssh://root:12345678@127.0.0.1:22/Users/TranceLove/My+Documents/%7BReference%7D%20Zobius%20Facro%20%24%24%20%23RFII1"); + + 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( + "defaultPath", "/Users/TranceLove/My Documents/{Reference} Zobius Facro $$ #RFII1"); + verify.putString( + "password", + PasswordUtil.INSTANCE + .encryptPassword(AppConfig.getInstance(), "12345678", Base64.URL_SAFE) + .replace("\n", "")); testOpenSftpConnectDialog(uri, verify); } @@ -180,7 +233,7 @@ private void testOpenSftpConnectDialog(String uri, Bundle verify) "SCP/SFTP Connection", NetCopyClientUtils.INSTANCE.encryptFtpPathAsNecessary(uri), true); assertEquals(1, mc.constructed().size()); SftpConnectDialog mocked = mc.constructed().get(0); - await().atMost(5, TimeUnit.SECONDS).until(() -> mocked.getArguments() != null); + await().atMost(999, TimeUnit.SECONDS).until(() -> mocked.getArguments() != null); for (String key : BUNDLE_KEYS) { if (mocked.getArguments().get(key) != null) { if (!key.equals("password")) { diff --git a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt index 635547e8f0..697fde2f18 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt @@ -36,7 +36,6 @@ import com.amaze.filemanager.utils.SmbUtil.getSmbEncryptedPath import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -96,7 +95,6 @@ class SmbUtilTest { * Test encrypt/decrypt URIs without password. It should stay the same too. */ @Test - @Ignore("Good idea to fix me") fun testEncryptWithoutPassword() { val path = "smb://toor@127.0.0.1" assertEquals( From 9d493ea77e00831cf61e92acd69acbd4bf50127f Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sun, 22 Jan 2023 15:26:51 +0800 Subject: [PATCH 057/384] Add handling of plus sign (+) in path name With test cases, with test cases to prove #2964 will work. Fixes #3560, fixes #3636, fixes #2964 --- .../filemanager/filesystem/HybridFile.java | 22 +-- ...ServiceAndroidFileSystemIntegrationTest.kt | 3 +- ...tFilesOnSshdTest.kt => FilesOnSshdTest.kt} | 137 +++++++++++++++--- 3 files changed, 128 insertions(+), 34 deletions(-) rename app/src/test/java/com/amaze/filemanager/filesystem/ssh/{ListFilesOnSshdTest.kt => FilesOnSshdTest.kt} (65%) 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 f768d9e22c..ce156566c0 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -420,7 +420,7 @@ public Long execute(@NonNull SFTPClient client) throws IOException { */ public String getPath() { try { - return URLDecoder.decode(path, "UTF-8"); + return URLDecoder.decode(path.replace("+", "%2b"), "UTF-8"); } catch (UnsupportedEncodingException | IllegalArgumentException e) { LOG.warn("failed to decode path {}", path, e); return path; @@ -550,7 +550,7 @@ public String getParent(Context context) { String thisPath = path; if (thisPath.contains("%")) { try { - thisPath = URLDecoder.decode(path, Charsets.UTF_8.name()); + thisPath = URLDecoder.decode(getPath(), Charsets.UTF_8.name()); } catch (UnsupportedEncodingException ignored) { } } @@ -940,12 +940,12 @@ public void forEachChildrenFile(Context context, boolean isRoot, OnFileFound onF switch (mode) { case SFTP: NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { + new SFtpClientTemplate(getPath(), false) { @Override public Boolean execute(@NonNull SFTPClient client) { try { for (RemoteResourceInfo info : - client.ls(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path))) { + client.ls(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath()))) { boolean isDirectory = false; try { isDirectory = SshClientUtils.isDirectory(client, info); @@ -953,7 +953,7 @@ public Boolean execute(@NonNull SFTPClient client) { LOG.warn("IOException checking isDirectory(): " + info.getPath()); continue; } - HybridFileParcelable f = new HybridFileParcelable(path, isDirectory, info); + HybridFileParcelable f = new HybridFileParcelable(getPath(), isDirectory, info); onFileFound.onFileFound(f); } } catch (IOException e) { @@ -990,10 +990,10 @@ public Boolean execute(@NonNull SFTPClient client) { } break; case FTP: - String thisPath = NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path); + String thisPath = NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath()); FTPFile[] ftpFiles = NetCopyClientUtils.INSTANCE.execute( - new FtpClientTemplate(path, false) { + new FtpClientTemplate(getPath(), false) { public FTPFile[] executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOException { ftpClient.changeWorkingDirectory(thisPath); @@ -1001,7 +1001,7 @@ public FTPFile[] executeWithFtpClient(@NonNull FTPClient ftpClient) } }); for (FTPFile ftpFile : ftpFiles) { - onFileFound.onFileFound(new HybridFileParcelable(path, ftpFile)); + onFileFound.onFileFound(new HybridFileParcelable(getPath(), ftpFile)); } break; case OTG: @@ -1089,11 +1089,11 @@ public InputStream getInputStream(Context context) { case SFTP: inputStream = SshClientUtils.execute( - new SFtpClientTemplate(path, false) { + new SFtpClientTemplate(getPath(), false) { @Override public InputStream execute(@NonNull final SFTPClient client) throws IOException { final RemoteFile rf = - client.open(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path)); + client.open(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath())); return rf.new RemoteFileInputStream() { @Override public void close() throws IOException { @@ -1119,7 +1119,7 @@ public void close() throws IOException { case FTP: inputStream = NetCopyClientUtils.INSTANCE.execute( - new FtpClientTemplate(path, false) { + new FtpClientTemplate(getPath(), false) { public InputStream executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOException { String parent = getParent(AppConfig.getInstance()); diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/FtpServiceAndroidFileSystemIntegrationTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/FtpServiceAndroidFileSystemIntegrationTest.kt index 0818042f71..1b728a2ff8 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/FtpServiceAndroidFileSystemIntegrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/FtpServiceAndroidFileSystemIntegrationTest.kt @@ -88,7 +88,8 @@ class FtpServiceAndroidFileSystemIntegrationTest { Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_DCIM, Environment.DIRECTORY_DOCUMENTS, - "1/2/3/4/5/6/7" + "1/2/3/4/5/6/7", + "lost+found" ) } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt similarity index 65% rename from app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.kt rename to app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt index 41bacb8736..013a0f09d1 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/ListFilesOnSshdTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt @@ -22,19 +22,22 @@ package com.amaze.filemanager.filesystem.ssh import android.os.Environment import androidx.test.core.app.ApplicationProvider +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFile import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.test.randomBytes import com.amaze.filemanager.utils.OnFileFound import org.awaitility.Awaitility.await import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test +import java.io.ByteArrayInputStream import java.io.File import java.io.FileOutputStream -import java.io.IOException import java.nio.file.Files import java.nio.file.Paths @@ -42,14 +45,14 @@ import java.nio.file.Paths * Test listing files on SSH server. */ @Suppress("StringLiteralDuplication") -class ListFilesOnSshdTest : AbstractSftpServerTest() { +class FilesOnSshdTest : AbstractSftpServerTest() { /** * Test list directories normally */ @Test fun testNormalListDirs() { - for (s in arrayOf("sysroot", "srv", "var", "tmp", "bin", "lib", "usr")) { + for (s in arrayOf("sysroot", "srv", "var", "tmp", "bin", "lib", "usr", "sysroot+v2")) { File(Environment.getExternalStorageDirectory(), s).mkdir() } assertTrue(performVerify()) @@ -59,26 +62,55 @@ class ListFilesOnSshdTest : AbstractSftpServerTest() { * Test list directories and symbolic links */ @Test - @Throws(Exception::class) fun testListDirsAndSymlinks() { createNecessaryDirsForSymlinkRelatedTests() assertTrue(performVerify()) } - @Throws(IOException::class) + /** + * Test listing directories and files with special characters. This includes colon signs, which + * is possible - even on Windows. See #2964 + */ + @Test + fun testListFilesWithSpecialChars() { + createFilesAndDirectoriesWithSpecialChars() + performVerify2() + val file = HybridFile( + OpenMode.SFTP, + "ssh://$USERNAME:$encryptedPassword@$HOST:$serverPort/sysroot%2Bv2/test%2Bfile.bin" + ) + val content = file.getInputStream(AppConfig.getInstance())?.readBytes() + assertNotNull(content) + assertTrue(true == content?.isNotEmpty()) + } + private fun createNecessaryDirsForSymlinkRelatedTests() { - val sysroot = File(Environment.getExternalStorageDirectory(), "sysroot") - sysroot.mkdir() - for (s in arrayOf("srv", "var", "tmp")) { - val subdir = File(sysroot, s) - subdir.mkdir() - Files.createSymbolicLink( - Paths.get(File(Environment.getExternalStorageDirectory(), s).absolutePath), - Paths.get(subdir.absolutePath) - ) + Environment.getExternalStorageDirectory().let { root -> + val sysroot = File(root, "sysroot") + sysroot.mkdir() + for (s in arrayOf("srv", "var", "tmp")) { + val subdir = File(sysroot, s) + subdir.mkdir() + Files.createSymbolicLink( + Paths.get(File(root, s).absolutePath), + Paths.get(subdir.absolutePath) + ) + } + for (s in arrayOf("bin", "lib", "usr")) { + File(root, s).mkdir() + } + File(root, "sysroot+v2").mkdirs() } - for (s in arrayOf("bin", "lib", "usr")) { - File(Environment.getExternalStorageDirectory(), s).mkdir() + } + + private fun createFilesAndDirectoriesWithSpecialChars() { + File(Environment.getExternalStorageDirectory(), "sysroot+v2").let { + it.mkdirs() + File(it, "D:").run { + mkdirs() + File(this, "Users").mkdirs() + } + ByteArrayInputStream(randomBytes()).copyTo(FileOutputStream(File(it, "test+file.bin"))) } } @@ -98,10 +130,71 @@ class ListFilesOnSshdTest : AbstractSftpServerTest() { } } ) - await().until { result.size == 7 } + await().until { result.size == 8 } + assertThat>( + result, + Matchers.hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr", "sysroot+v2") + ) + return true + } + + private fun performVerify2(): Boolean { + val result: MutableList = ArrayList() + var file = HybridFile( + OpenMode.SFTP, + "ssh://$USERNAME:$encryptedPassword@$HOST:$serverPort/sysroot%2Bv2" + ) + file.forEachChildrenFile( + ApplicationProvider.getApplicationContext(), + false, + object : OnFileFound { + override fun onFileFound(fileFound: HybridFileParcelable) { + result.add(fileFound.name) + } + } + ) + await().until { result.size == 2 } + assertThat>( + result, + Matchers.hasItems("test+file.bin", "D:") + ) + result.clear() + file = HybridFile( + OpenMode.SFTP, + "ssh://$USERNAME:$encryptedPassword@$HOST:$serverPort/sysroot%2Bv2/D:" + ) + file.forEachChildrenFile( + ApplicationProvider.getApplicationContext(), + false, + object : OnFileFound { + override fun onFileFound(fileFound: HybridFileParcelable) { + result.add(fileFound.name) + } + } + ) + await().until { result.size == 1 } + assertThat>( + result, + Matchers.hasItems("Users") + ) + result.clear() + file = HybridFile( + OpenMode.SFTP, + "ssh://$USERNAME:$encryptedPassword@$HOST:$serverPort/sysroot%2Bv2/D%3A" + ) + file.forEachChildrenFile( + ApplicationProvider.getApplicationContext(), + false, + object : OnFileFound { + override fun onFileFound(fileFound: HybridFileParcelable) { + result.add(fileFound.name) + } + } + ) + await().until { result.size == 1 } assertThat>( result, - Matchers.hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr") + Matchers.hasItems("Users") ) return true } @@ -153,10 +246,10 @@ class ListFilesOnSshdTest : AbstractSftpServerTest() { } } ) - await().until { dirs.size == 7 } + await().until { dirs.size == 8 } assertThat>( dirs, - Matchers.hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr") + Matchers.hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr", "sysroot+v2") ) assertThat>( files, @@ -236,10 +329,10 @@ class ListFilesOnSshdTest : AbstractSftpServerTest() { } } ) - await().until { result2.size == 7 } + await().until { result2.size == 8 } assertThat>( result2, - Matchers.hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr") + Matchers.hasItems("sysroot", "srv", "var", "tmp", "bin", "lib", "usr", "sysroot+v2") ) } } From 335edd6aba2fde36c66848c66f960b99771deb4e Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sun, 22 Jan 2023 17:54:24 +0800 Subject: [PATCH 058/384] NetCopyClientUtils.deriveUriFrom() remove keypair argument Fixes problem with dropping connection from drawer --- .../ftp/NetCopyClientConnectionPool.kt | 3 +- .../filesystem/ftp/NetCopyClientUtils.kt | 15 +++++----- .../ui/dialogs/SftpConnectDialog.kt | 12 +++----- .../ssh/SshAuthenticationTaskTest.kt | 2 +- .../filesystem/ftp/NetCopyClientUtilTest.kt | 30 +++++++++++++++++++ .../filesystem/ssh/SshClientUtilsTest.kt | 6 ++-- 6 files changed, 46 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt index fb9c8837e5..c913dbe67f 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt @@ -141,8 +141,7 @@ object NetCopyClientConnectionPool { port, "", username, - password, - keyPair + password ) var client = connections[url] if (client == null) { diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt index ed3c5ce77e..f2f13664c4 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt @@ -47,7 +47,6 @@ import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPReply import org.slf4j.LoggerFactory import java.io.IOException -import java.security.KeyPair object NetCopyClientUtils { @@ -222,18 +221,20 @@ object NetCopyClientUtils { port: Int, defaultPath: String? = null, username: String, - password: String? = null, - selectedParsedKeyPair: KeyPair? = null + password: String? = null ): String { // FIXME: should be caller's responsibility var pathSuffix = defaultPath if (pathSuffix == null) pathSuffix = SLASH.toString() - return if (selectedParsedKeyPair != null) { - "$prefix$username@$hostname:$port$pathSuffix" - } else if (username == "" && (password == "" || password == null)) { + val thisPassword = if (password == "" || password == null) { + "" + } else { + ":$password" + } + return if (username == "" && (true == password?.isEmpty())) { "$prefix$hostname:$port$pathSuffix" } else { - "$prefix$username:$password@$hostname:$port$pathSuffix" + "$prefix$username$thisPassword@$hostname:$port$pathSuffix" } } 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 081aa5b803..93067fdc4b 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 @@ -248,8 +248,7 @@ class SftpConnectDialog : DialogFragment() { requireArguments().getInt(ARG_PORT), requireArguments().getString(ARG_DEFAULT_PATH, ""), requireArguments().getString(ARG_USERNAME)!!, - requireArguments().getString(ARG_PASSWORD), - selectedParsedKeyPair + requireArguments().getString(ARG_PASSWORD) ) ) } @@ -269,8 +268,7 @@ class SftpConnectDialog : DialogFragment() { port, defaultPath, username, - requireArguments().getString(ARG_PASSWORD, null), - selectedParsedKeyPair + requireArguments().getString(ARG_PASSWORD, null) ) ) val i = DataUtils.getInstance().containsServer( @@ -363,8 +361,7 @@ class SftpConnectDialog : DialogFragment() { port, defaultPath, username, - arguments?.getString(ARG_PASSWORD, null), - selectedParsedKeyPair + arguments?.getString(ARG_PASSWORD, null) ) )?.let { sshHostKey -> NetCopyClientConnectionPool.removeConnection( @@ -765,8 +762,7 @@ class SftpConnectDialog : DialogFragment() { port, defaultPath, username, - password, - selectedParsedKeyPair + password ) } diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt index 6225c84705..491b7dd717 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh import android.content.Context -import android.os.Build.VERSION_CODES.JELLY_BEAN +import android.os.Build import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt index d1c1b29eae..07feec3382 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtilTest.kt @@ -25,6 +25,36 @@ import org.junit.Test @Suppress("StringLiteralDuplication") class NetCopyClientUtilTest { + + /** + * Test [NetCopyClientUtils.deriveUriFrom]. + */ + @Test + fun testDeriveUriFrom() { + assertEquals( + "ssh://user:password@127.0.0.1:22222/", + NetCopyClientUtils.deriveUriFrom( + prefix = "ssh://", + username = "user", + password = "password", + hostname = "127.0.0.1", + port = 22222, + defaultPath = null + ) + ) + assertEquals( + "ssh://user@127.0.0.1:22222/", + NetCopyClientUtils.deriveUriFrom( + prefix = "ssh://", + username = "user", + password = null, + hostname = "127.0.0.1", + port = 22222, + defaultPath = null + ) + ) + } + /** * Test [NetCopyClientUtils.extractRemotePathFrom]. */ diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshClientUtilsTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshClientUtilsTest.kt index 3f12326c5d..0954732853 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshClientUtilsTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshClientUtilsTest.kt @@ -57,8 +57,7 @@ class SshClientUtilsTest { 22, null, "root", - "toor", - null + "toor" ) ) assertEquals( @@ -69,8 +68,7 @@ class SshClientUtilsTest { 22, "", "root", - "toor", - null + "toor" ) ) } From 64328e6af63c25130e7fba6836eabec5f2d35c69 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Mon, 30 Jan 2023 22:54:20 +0800 Subject: [PATCH 059/384] Test fixes --- .../asynctasks/ssh/SshAuthenticationTaskTest.kt | 13 ++++++++++--- .../filesystem/ftp/NetCopyConnectionInfoTest.kt | 4 ++-- .../filesystem/ssh/AbstractSftpServerTest.kt | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt index 491b7dd717..5f7f0cfc8e 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt @@ -27,9 +27,12 @@ import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.R +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool import com.amaze.filemanager.filesystem.ssh.test.TestKeyProvider import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.test.ShadowPasswordUtil +import com.amaze.filemanager.utils.PasswordUtil import io.reactivex.Single import io.reactivex.android.plugins.RxAndroidPlugins import io.reactivex.android.schedulers.AndroidSchedulers @@ -61,7 +64,10 @@ import java.net.SocketException import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P, Build.VERSION_CODES.R]) +@Config( + shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], + sdk = [KITKAT, P, Build.VERSION_CODES.R] +) @Suppress("StringLiteralDuplication") class SshAuthenticationTaskTest { @@ -95,7 +101,7 @@ class SshAuthenticationTaskTest { hostname = "127.0.0.1", port = 22222, username = "user", - password = "password" + password = PasswordUtil.encryptPassword(AppConfig.getInstance(), "password") ) val latch = CountDownLatch(1) var e: Throwable? = null @@ -108,6 +114,7 @@ class SshAuthenticationTaskTest { result = it latch.countDown() }, { + it.printStackTrace() task.onError(it) e = it latch.countDown() @@ -282,7 +289,7 @@ class SshAuthenticationTaskTest { hostname = "127.0.0.1", port = 22222, username = "user", - password = "password" + password = PasswordUtil.encryptPassword(AppConfig.getInstance(), "password") ) val latch = CountDownLatch(1) var e: Throwable? = null diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt index 1b141d0857..774ce3fef0 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt @@ -20,7 +20,7 @@ package com.amaze.filemanager.filesystem.ftp -import android.os.Build.VERSION_CODES.JELLY_BEAN +import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -36,7 +36,7 @@ import java.net.URLEncoder.encode /* ktlint-disable max-line-length */ @RunWith(AndroidJUnit4::class) @Config( - sdk = [JELLY_BEAN, KITKAT, P], + sdk = [KITKAT, P, VERSION_CODES.R], shadows = [ShadowPasswordUtil::class, ShadowMultiDex::class] ) @Suppress("StringLiteralDuplication") diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt index aa5ca551b7..18008f0466 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt @@ -20,7 +20,7 @@ package com.amaze.filemanager.filesystem.ssh -import android.os.Build.VERSION_CODES.JELLY_BEAN +import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES.KITKAT import android.os.Build.VERSION_CODES.P import android.os.Environment @@ -62,7 +62,7 @@ import java.nio.file.Paths @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [JELLY_BEAN, KITKAT, P] + sdk = [KITKAT, P, VERSION_CODES.R] ) abstract class AbstractSftpServerTest { From 71db59f6f9b7109112830705d4da0bdcd0e225ce Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 21 Feb 2023 23:07:30 +0530 Subject: [PATCH 060/384] skip repeated queries and empty strings --- .../java/com/amaze/filemanager/ui/views/appbar/SearchView.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 6a45f34055..c9d858c863 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -111,6 +111,8 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener .fromJson(preferenceString, new TypeToken>() {}.getType()) : new ArrayList<>(); + if (s.isEmpty() || recentSearches.contains(s)) return false; + recentSearches.add(s); if (recentSearches.size() > 5) recentSearches.remove(0); From c02806ebba6dffc240ee3fbeb759dc8ed0244f3f Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Wed, 22 Feb 2023 22:01:21 +0530 Subject: [PATCH 061/384] chore: fix codady --- .../com/amaze/filemanager/ui/views/appbar/SearchView.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index c9d858c863..efb717407c 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -65,7 +65,10 @@ public class SearchView { private final ConstraintLayout searchViewLayout; private final AppCompatEditText searchViewEditText; - private final ImageView clearImageView, backImageView; + + private final ImageView clearImageView; + private final ImageView backImageView; + private final TextView recentHintTV; private final ChipGroup recentChipGroup; From 0657f9dd5a52b6fbc09cd5d266b0103ef0b02b24 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 23 Feb 2023 12:38:41 +0530 Subject: [PATCH 062/384] chore: fix failing CI --- app/src/main/res/layout/layout_search.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/layout_search.xml b/app/src/main/res/layout/layout_search.xml index 0da790bdf4..04bc137d5e 100644 --- a/app/src/main/res/layout/layout_search.xml +++ b/app/src/main/res/layout/layout_search.xml @@ -9,17 +9,17 @@ android:gravity="center_vertical" android:visibility="gone"> - + app:layout_constraintTop_toTopOf="@id/search_edit_text" + app:srcCompat="@drawable/ic_arrow_back_black_24dp" /> - + app:layout_constraintTop_toTopOf="@id/search_edit_text" + app:srcCompat="@drawable/ic_close_black_24dp" /> Date: Thu, 2 Feb 2023 00:01:04 +0800 Subject: [PATCH 063/384] Changes per PR feedback - Fix password authentication regressions - Add routine to cleanup path as necessary, to prevent duplicate slashes display at path (#3739) --- .../ftp/auth/FtpAuthenticationTaskCallable.kt | 2 +- .../ssh/SshAuthenticationTaskCallable.kt | 9 +- .../filemanager/database/UtilsHandler.kt | 32 +- .../filemanager/filesystem/HybridFile.java | 55 ++-- .../ftp/NetCopyClientConnectionPool.kt | 1 + .../filesystem/ftp/NetCopyClientUtils.kt | 40 ++- .../filesystem/ftp/NetCopyConnectionInfo.kt | 42 ++- .../ui/activities/MainActivity.java | 29 +- .../ui/dialogs/SftpConnectDialog.kt | 80 +++-- .../ui/dialogs/SmbConnectDialog.java | 175 ++++++---- .../com/amaze/filemanager/utils/SmbUtil.kt | 21 +- .../filemanager/database/UtilsHandlerTest.kt | 9 +- .../filemanager/filesystem/HybridFileTest.kt | 43 +++ .../ftp/NetCopyClientConnectionPoolFtpTest.kt | 13 +- .../ftp/NetCopyConnectionInfoTest.kt | 46 ++- .../filesystem/ssh/AbstractSftpServerTest.kt | 14 +- .../filesystem/ssh/FilesOnSshdTest.kt | 3 +- .../ssh/NetCopyClientConnectionPoolSshTest.kt | 22 +- .../filesystem/ssh/test/TestUtils.kt | 17 +- .../AbstractMainActivityTestBase.kt | 106 ++++++ .../ui/activities/MainActivityTest.java | 308 ------------------ .../ui/activities/MainActivityTest.kt | 99 ++++++ .../ui/dialogs/SftpConnectDialogTest.kt | 219 +++++++++++++ .../ui/dialogs/SmbConnectDialogTest.kt | 109 +++++++ .../shadows/jcifs/smb/ShadowSmbFile.java | 3 +- 25 files changed, 937 insertions(+), 560 deletions(-) create mode 100644 app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt delete mode 100644 app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.java create mode 100644 app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.kt create mode 100644 app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogTest.kt create mode 100644 app/src/test/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialogTest.kt diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTaskCallable.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTaskCallable.kt index fdd5784a3a..021663f89b 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTaskCallable.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ftp/auth/FtpAuthenticationTaskCallable.kt @@ -53,7 +53,7 @@ open class FtpAuthenticationTaskCallable( ) } else { ftpClient.login( - username, + decode(username, UTF_8.name()), decode( PasswordUtil.decryptPassword(AppConfig.getInstance(), password), UTF_8.name() diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskCallable.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskCallable.kt index 5859931b30..0b35d3930c 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskCallable.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskCallable.kt @@ -61,7 +61,7 @@ class SshAuthenticationTaskCallable( sshClient.connect(hostname, port) if (privateKey != null) { sshClient.authPublickey( - username, + decode(username, UTF_8.name()), object : KeyProvider { override fun getPrivate(): PrivateKey = privateKey.private @@ -73,9 +73,12 @@ class SshAuthenticationTaskCallable( sshClient } else { sshClient.authPassword( - username, + decode(username, UTF_8.name()), decode( - PasswordUtil.decryptPassword(AppConfig.getInstance(), password!!), + PasswordUtil.decryptPassword( + AppConfig.getInstance(), + password!! + ), UTF_8.name() ) ) diff --git a/app/src/main/java/com/amaze/filemanager/database/UtilsHandler.kt b/app/src/main/java/com/amaze/filemanager/database/UtilsHandler.kt index 564ff72856..6a194585fb 100644 --- a/app/src/main/java/com/amaze/filemanager/database/UtilsHandler.kt +++ b/app/src/main/java/com/amaze/filemanager/database/UtilsHandler.kt @@ -25,7 +25,6 @@ import android.os.Environment import android.widget.Toast import com.amaze.filemanager.BuildConfig import com.amaze.filemanager.R -import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.database.models.OperationData import com.amaze.filemanager.database.models.utilities.Bookmark import com.amaze.filemanager.database.models.utilities.Grid @@ -33,7 +32,6 @@ import com.amaze.filemanager.database.models.utilities.Hidden 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.utils.SmbUtil import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFactory import com.googlecode.concurrenttrees.radix.node.concrete.voidvalue.VoidValue @@ -289,8 +287,7 @@ class UtilsHandler( .blockingGet() ) { try { - val path = SmbUtil.getSmbDecryptedPath(context, entry.path) - retval.add(arrayOf(entry.name, path)) + retval.add(arrayOf(entry.name, entry.path)) } catch (e: GeneralSecurityException) { log.warn("failed to decrypt smb list path", e) @@ -407,16 +404,10 @@ class UtilsHandler( * must encrypt it's password fiend first first */ private fun removeSmbPath(name: String, path: String) { - var path = path - if ("" == path) utilitiesDatabase.smbEntryDao().deleteByName(name) - .subscribeOn(Schedulers.io()).subscribe() else { - try { - path = SmbUtil.getSmbEncryptedPath(context, path) - } catch (e: GeneralSecurityException) { - log.error("Error encrypting path", e) - } catch (e: IOException) { - log.error("Error encrypting path", e) - } + if ("" == path) { + utilitiesDatabase.smbEntryDao().deleteByName(name) + .subscribeOn(Schedulers.io()).subscribe() + } else { utilitiesDatabase .smbEntryDao() .deleteByNameAndPath(name, path) @@ -464,24 +455,13 @@ class UtilsHandler( * Update [SmbEntry]. */ fun renameSMB(oldName: String, oldPath: String, newName: String, newPath: String) { - var oldPath = oldPath - var newPath = newPath - try { - oldPath = SmbUtil.getSmbEncryptedPath(AppConfig.getInstance(), oldPath) - newPath = SmbUtil.getSmbEncryptedPath(AppConfig.getInstance(), newPath) - } catch (e: GeneralSecurityException) { - log.error("Error encrypting SMB path", e) - } catch (e: IOException) { - log.error("Error encrypting SMB path", e) - } - val finalNewPath = newPath utilitiesDatabase .smbEntryDao() .findByNameAndPath(oldName, oldPath) .subscribeOn(Schedulers.io()) .subscribe { smbEntry: SmbEntry -> smbEntry.name = newName - smbEntry.path = finalNewPath + smbEntry.path = newPath utilitiesDatabase .smbEntryDao() .update(smbEntry) 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 ce156566c0..289020cdbf 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -24,6 +24,7 @@ import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX; import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX; import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX; +import static com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.MULTI_SLASH; import static com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX; import java.io.File; @@ -102,6 +103,7 @@ import androidx.documentfile.provider.DocumentFile; import androidx.preference.PreferenceManager; +import io.reactivex.Flowable; import io.reactivex.Single; import io.reactivex.SingleObserver; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -118,14 +120,13 @@ import net.schmizz.sshj.connection.channel.direct.Session; import net.schmizz.sshj.sftp.FileMode; import net.schmizz.sshj.sftp.RemoteFile; -import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.sftp.SFTPClient; import net.schmizz.sshj.sftp.SFTPException; /** Hybrid file for handeling all types of files */ public class HybridFile { - private final Logger LOG = LoggerFactory.getLogger(HybridFile.class); + private static final Logger LOG = LoggerFactory.getLogger(HybridFile.class); public static final String DOCUMENT_FILE_PREFIX = "content://com.android.externalstorage.documents"; @@ -139,6 +140,7 @@ public class HybridFile { public HybridFile(OpenMode mode, String path) { this.path = path; this.mode = mode; + sanitizePathAsNecessary(); } public HybridFile(OpenMode mode, String path, String name, boolean isDirectory) { @@ -158,6 +160,7 @@ public HybridFile(OpenMode mode, String path, String name, boolean isDirectory) } else { this.path += "/" + name; } + sanitizePathAsNecessary(); } public void generateMode(Context context) { @@ -944,18 +947,28 @@ public void forEachChildrenFile(Context context, boolean isRoot, OnFileFound onF @Override public Boolean execute(@NonNull SFTPClient client) { try { - for (RemoteResourceInfo info : - client.ls(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath()))) { - boolean isDirectory = false; - try { - isDirectory = SshClientUtils.isDirectory(client, info); - } catch (IOException ifBrokenSymlink) { - LOG.warn("IOException checking isDirectory(): " + info.getPath()); - continue; - } - HybridFileParcelable f = new HybridFileParcelable(getPath(), isDirectory, info); - onFileFound.onFileFound(f); - } + Flowable.fromIterable( + client.ls(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath()))) + .onBackpressureBuffer() + .subscribeOn(Schedulers.computation()) + .map( + info -> { + boolean isDirectory = false; + try { + isDirectory = SshClientUtils.isDirectory(client, info); + } catch (IOException ifBrokenSymlink) { + LOG.warn("IOException checking isDirectory(): " + info.getPath()); + return Flowable.empty(); + } + return new HybridFileParcelable(getPath(), isDirectory, info); + }) + .doOnNext( + v -> { + if (v instanceof HybridFileParcelable) { + onFileFound.onFileFound((HybridFileParcelable) v); + } + }) + .blockingSubscribe(); } catch (IOException e) { LOG.warn("IOException", e); AppConfig.toast( @@ -1067,14 +1080,6 @@ private static String formatUriForDisplayInternal( return String.format("%s://%s%s", scheme, host, path); } - /** - * @deprecated use {@link #getInputStream(Context)} which allows handling content resolver - */ - @Nullable - public InputStream getInputStream() { - return getInputStream(AppConfig.getInstance()); - } - /** * Handles getting input stream for various {@link OpenMode} * @@ -1725,7 +1730,7 @@ public void onSuccess(String t) { @Override public void onError(Throwable e) { - LOG.warn("failed to get sha256 for sftp file", e); + LOG.warn("failed to get sha256 for file", e); callback.apply(context.getString(R.string.error)); } }); @@ -1808,4 +1813,8 @@ private void openFileInternal(MainActivity activity) { break; } } + + private void sanitizePathAsNecessary() { + this.path = this.path.replaceAll(MULTI_SLASH, "/"); + } } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt index c913dbe67f..e5fb262932 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPool.kt @@ -212,6 +212,7 @@ object NetCopyClientConnectionPool { fun removeConnection(url: String, callback: () -> Unit) { Maybe.fromCallable(AsyncRemoveConnection(url)) .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) .subscribe { callback.invoke() } } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt index f2f13664c4..372bc5279f 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt @@ -39,6 +39,7 @@ import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.SLAS import com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate import com.amaze.filemanager.utils.SmbUtil +import com.amaze.filemanager.utils.urlEncoded import io.reactivex.Maybe import io.reactivex.Scheduler import io.reactivex.schedulers.Schedulers @@ -112,16 +113,14 @@ object NetCopyClientUtils { * @return SSH URL with the password (if exists) encrypted */ fun encryptFtpPathAsNecessary(fullUri: String): String { - return NetCopyConnectionInfo(fullUri).run { - val uriWithoutProtocol: String = fullUri.substring(prefix.length) - if (uriWithoutProtocol.substringBefore(AT).indexOf(COLON) > 0) { - SmbUtil.getSmbEncryptedPath( - AppConfig.getInstance(), - fullUri - ) - } else { + val uriWithoutProtocol: String = fullUri.substringAfter("://") + return if (uriWithoutProtocol.substringBefore(AT).indexOf(COLON) > 0) { + SmbUtil.getSmbEncryptedPath( + AppConfig.getInstance(), fullUri - } + ) + } else { + fullUri } } @@ -133,12 +132,16 @@ object NetCopyClientUtils { * @return SSH URL with the password (if exists) decrypted */ fun decryptFtpPathAsNecessary(fullUri: String): String { - return NetCopyConnectionInfo(fullUri).runCatching { - val uriWithoutProtocol: String = fullUri.substring(prefix.length) - if (uriWithoutProtocol.lastIndexOf(COLON) > 0) SmbUtil.getSmbDecryptedPath( - AppConfig.getInstance(), + return runCatching { + val uriWithoutProtocol: String = fullUri.substringAfter("://") + if (uriWithoutProtocol.lastIndexOf(COLON) > 0) { + SmbUtil.getSmbDecryptedPath( + AppConfig.getInstance(), + fullUri + ) + } else { fullUri - ) else fullUri + } }.getOrElse { e -> LOG.error("Error decrypting path", e) fullUri @@ -221,7 +224,8 @@ object NetCopyClientUtils { port: Int, defaultPath: String? = null, username: String, - password: String? = null + password: String? = null, + edit: Boolean = false ): String { // FIXME: should be caller's responsibility var pathSuffix = defaultPath @@ -229,7 +233,11 @@ object NetCopyClientUtils { val thisPassword = if (password == "" || password == null) { "" } else { - ":$password" + ":${if (edit) { + password + } else { + password.urlEncoded() + }}" } return if (username == "" && (true == password?.isEmpty())) { "$prefix$hostname:$port$pathSuffix" diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt index ea61465d99..bac9fa83b7 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfo.kt @@ -25,7 +25,6 @@ import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_ import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLON import com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX -import java.net.URLDecoder.decode /** * Container object for SSH/FTP/FTPS URL, encapsulating logic for splitting information from given @@ -64,9 +63,12 @@ class NetCopyConnectionInfo(url: String) { // (No, don't break it down to lines) /* ktlint-disable max-line-length */ - private const val URI_REGEX = "^(?:(?![^:@]+:[^:@\\/]*@)([^:\\/?#.]+):)?(?:\\/\\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\\/?#]*)(?::(\\d*))?)(((\\/(?:[^?#](?![^?#\\/]*\\.[^?#\\/.]+(?:[?#]|$)))*\\/?)?([^?#\\/]*))(?:\\?([^#]*))?(?:#(.*))?)" + private const val URI_REGEX = "^(?:(?![^:@]+:[^:@/]*@)([^:/?#.]+):)?(?://)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:/?#]*)(?::(\\d*))?)(((/(?:[^?#](?![^?#/]*\\.[^?#/.]+(?:[?#]|$)))*/?)?([^?#/]*))(?:\\?([^#]*))?(?:#(.*))?)" /* ktlint-enable max-line-length */ + + const val MULTI_SLASH = "(?<=[^:])(//+)" + const val AND = '&' const val AT = '@' const val SLASH = '/' @@ -80,7 +82,7 @@ class NetCopyConnectionInfo(url: String) { url.startsWith(FTPS_URI_PREFIX) or url.startsWith(SMB_URI_PREFIX) ) { - "Argument is not a SSH URI: $url" + "Argument is not a supported remote URI: $url" } val regex = Regex(URI_REGEX) val matches = regex.find(url) @@ -92,11 +94,11 @@ class NetCopyConnectionInfo(url: String) { host = it[6] val credential = it[3] if (!credential.contains(COLON)) { - username = decode(credential, Charsets.UTF_8.name()) + username = credential password = null } else { - username = decode(credential.substringBefore(COLON), Charsets.UTF_8.name()) - password = decode(credential.substringAfter(COLON), Charsets.UTF_8.name()) + username = credential.substringBefore(COLON) + password = credential.substringAfter(COLON) } port = if (it[7].isNotEmpty()) { /* @@ -121,19 +123,21 @@ class NetCopyConnectionInfo(url: String) { } else { null } - defaultPath = if (it[9].isEmpty()) { - null - } else if (it[9] == SLASH.toString()) { - SLASH.toString() - } else if (!it[9].endsWith(SLASH)) { - if (it[11].isEmpty()) { - it[10] + defaultPath = ( + if (it[9].isEmpty()) { + null + } else if (it[9] == SLASH.toString()) { + SLASH.toString() + } else if (!it[9].endsWith(SLASH)) { + if (it[11].isEmpty()) { + it[10] + } else { + it[10].substringBeforeLast(SLASH) + } } else { - it[10].substringBeforeLast(SLASH) + it[9] } - } else { - it[9] - } + )?.replace(Regex(MULTI_SLASH), SLASH.toString()) filename = it[11].ifEmpty { null } } } @@ -147,7 +151,9 @@ class NetCopyConnectionInfo(url: String) { filename } else if (defaultPath != null && true == defaultPath?.isNotEmpty()) { defaultPath!!.substringAfterLast(SLASH) - } else null + } else { + null + } } override fun toString(): String { 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 80a35caf0d..04cd312614 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 @@ -522,6 +522,7 @@ public void invalidateFragmentAndBundle(Bundle savedInstanceState, boolean isClo } @Override + @SuppressLint("CheckResult") public void onPermissionGranted() { drawer.refreshDrawer(); TabFragment tabFragment = getTabFragment(); @@ -1975,11 +1976,11 @@ public void showSMBDialog(String name, String path, boolean edit) { } SmbConnectDialog smbConnectDialog = new SmbConnectDialog(); Bundle bundle = new Bundle(); - bundle.putString("name", name); - bundle.putString("path", path); - bundle.putBoolean("edit", edit); + bundle.putString(SmbConnectDialog.ARG_NAME, name); + bundle.putString(SmbConnectDialog.ARG_PATH, path); + bundle.putBoolean(SmbConnectDialog.ARG_EDIT, edit); smbConnectDialog.setArguments(bundle); - smbConnectDialog.show(getFragmentManager(), "smbdailog"); + smbConnectDialog.show(getSupportFragmentManager(), SmbConnectDialog.TAG); } @SuppressLint("CheckResult") @@ -2049,13 +2050,12 @@ public void hideSmokeScreen() { public void addConnection( boolean edit, @NonNull final String name, - @NonNull final String path, - @Nullable final String encryptedPath, + @NonNull final String encryptedPath, @Nullable final String oldname, @Nullable final String oldPath) { - String[] s = new String[] {name, path}; + String[] s = new String[] {name, encryptedPath}; if (!edit) { - if ((dataUtils.containsServer(path)) == -1) { + if ((dataUtils.containsServer(encryptedPath)) == -1) { Completable.fromRunnable( () -> utilsHandler.saveToDatabase( @@ -2069,7 +2069,7 @@ public void addConnection( // grid.addPath(name, encryptedPath, DataUtils.SMB, 1); executeWithMainFragment( mainFragment -> { - mainFragment.loadlist(path, false, OpenMode.UNKNOWN, true); + mainFragment.loadlist(encryptedPath, false, OpenMode.UNKNOWN, true); return null; }, true); @@ -2085,12 +2085,13 @@ public void addConnection( int i = dataUtils.containsServer(new String[] {oldname, oldPath}); if (i != -1) { dataUtils.removeServer(i); - - AppConfig.getInstance() - .runInBackground( + Flowable.fromCallable( () -> { - utilsHandler.renameSMB(oldname, oldPath, name, path); - }); + utilsHandler.renameSMB(oldname, oldPath, name, encryptedPath); + return true; + }) + .subscribeOn(Schedulers.io()) + .subscribe(); // mainActivity.grid.removePath(oldname, oldPath, DataUtils.SMB); } dataUtils.addServer(s); 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 93067fdc4b..e4cbda8de7 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 @@ -56,6 +56,7 @@ import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLON import com.amaze.filemanager.ui.activities.MainActivity import com.amaze.filemanager.ui.activities.superclasses.ThemedActivity import com.amaze.filemanager.ui.icons.MimeTypes @@ -63,10 +64,11 @@ import com.amaze.filemanager.ui.provider.UtilitiesProvider import com.amaze.filemanager.utils.BookSorter import com.amaze.filemanager.utils.DataUtils import com.amaze.filemanager.utils.MinMaxInputFilter +import com.amaze.filemanager.utils.PasswordUtil import com.amaze.filemanager.utils.SimpleTextWatcher import com.amaze.filemanager.utils.X509CertificateUtil.FINGERPRINT +import com.amaze.filemanager.utils.urlEncoded import com.google.android.material.snackbar.Snackbar -import io.reactivex.Observable import io.reactivex.Observable.create import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers @@ -241,15 +243,14 @@ class SftpConnectDialog : DialogFragment() { selectedParsedKeyPairName = requireArguments().getString(ARG_KEYPAIR_NAME) selectPemBTN.text = selectedParsedKeyPairName } - oldPath = NetCopyClientUtils.encryptFtpPathAsNecessary( - NetCopyClientUtils.deriveUriFrom( - requireArguments().getString(ARG_PROTOCOL)!!, - requireArguments().getString(ARG_ADDRESS)!!, - requireArguments().getInt(ARG_PORT), - requireArguments().getString(ARG_DEFAULT_PATH, ""), - requireArguments().getString(ARG_USERNAME)!!, - requireArguments().getString(ARG_PASSWORD) - ) + oldPath = NetCopyClientUtils.deriveUriFrom( + requireArguments().getString(ARG_PROTOCOL)!!, + requireArguments().getString(ARG_ADDRESS)!!, + requireArguments().getInt(ARG_PORT), + requireArguments().getString(ARG_DEFAULT_PATH, ""), + requireArguments().getString(ARG_USERNAME)!!, + requireArguments().getString(ARG_PASSWORD), + edit ) } } @@ -257,19 +258,18 @@ class SftpConnectDialog : DialogFragment() { private fun appendButtonListenersForEdit( dialogBuilder: MaterialDialog.Builder ) { - createConnectionSettings().run { + createConnectionSettings(edit = true).run { dialogBuilder .negativeText(R.string.delete) .onNegative { dialog: MaterialDialog, _: DialogAction? -> - val path = NetCopyClientUtils.encryptFtpPathAsNecessary( - NetCopyClientUtils.deriveUriFrom( - getProtocolPrefixFromDropdownSelection(), - hostname, - port, - defaultPath, - username, - requireArguments().getString(ARG_PASSWORD, null) - ) + val path = NetCopyClientUtils.deriveUriFrom( + getProtocolPrefixFromDropdownSelection(), + hostname, + port, + defaultPath, + username, + requireArguments().getString(ARG_PASSWORD, null), + edit = true ) val i = DataUtils.getInstance().containsServer( arrayOf(connectionName, path) @@ -292,7 +292,7 @@ class SftpConnectDialog : DialogFragment() { (activity as MainActivity).drawer.refreshDrawer() } dialog.dismiss() - }.neutralText(R.string.cancel) + }.neutralText(android.R.string.cancel) .onNeutral { dialog: MaterialDialog, _: DialogAction? -> dialog.dismiss() } } } @@ -334,7 +334,7 @@ class SftpConnectDialog : DialogFragment() { private fun handleOnPositiveButton(edit: Boolean): MaterialDialog.SingleButtonCallback = MaterialDialog.SingleButtonCallback { _, _ -> - createConnectionSettings().run { + createConnectionSettings(edit).run { when (prefix) { FTP_URI_PREFIX -> positiveButtonForFtp(this, edit) else -> positiveButtonForSftp(this, edit) @@ -361,7 +361,8 @@ class SftpConnectDialog : DialogFragment() { port, defaultPath, username, - arguments?.getString(ARG_PASSWORD, null) + requireArguments().getString(ARG_PASSWORD, null), + edit ) )?.let { sshHostKey -> NetCopyClientConnectionPool.removeConnection( @@ -469,7 +470,7 @@ class SftpConnectDialog : DialogFragment() { this, StringBuilder(hostname).also { if (port != NetCopyClientConnectionPool.SSH_DEFAULT_PORT && port > 0) { - it.append(':').append(port) + it.append(COLON).append(port) } }.toString(), hostKey.algorithm, @@ -583,7 +584,7 @@ class SftpConnectDialog : DialogFragment() { .observeOn(AndroidSchedulers.mainThread()) .retryWhen { exceptions -> exceptions.flatMap { exception -> - Observable.create { subscriber -> + create { subscriber -> observable.displayPassphraseDialog(exception, { subscriber.onNext(Unit) }, { @@ -624,7 +625,6 @@ class SftpConnectDialog : DialogFragment() { saveFtpConnectionAndLoadlist( connectionSettings, hostKeyFingerprint, - path, encryptedPath, selectedParsedKeyPairName, selectedParsedKeyPair @@ -642,7 +642,6 @@ class SftpConnectDialog : DialogFragment() { private fun saveFtpConnectionAndLoadlist( connectionSettings: ConnectionSettings, hostKeyFingerprint: String?, - path: String, encryptedPath: String, selectedParsedKeyPairName: String?, selectedParsedKeyPair: KeyPair? @@ -655,11 +654,15 @@ class SftpConnectDialog : DialogFragment() { port, hostKeyFingerprint, username, - password, + if (false == password?.isBlank()) { + PasswordUtil.encryptPassword(requireContext(), password)?.replace("\n", "") + } else { + password + }, selectedParsedKeyPair )?.run { - if (DataUtils.getInstance().containsServer(path) == -1) { - DataUtils.getInstance().addServer(arrayOf(connectionName, path)) + if (DataUtils.getInstance().containsServer(encryptedPath) == -1) { + DataUtils.getInstance().addServer(arrayOf(connectionName, encryptedPath)) (activity as MainActivity).drawer.refreshDrawer() AppConfig.getInstance().utilsHandler.saveToDatabase( OperationData( @@ -673,7 +676,7 @@ class SftpConnectDialog : DialogFragment() { ) val ma = (activity as MainActivity).currentMainFragment ma?.loadlist( - path, + encryptedPath, false, if (prefix == SSH_URI_PREFIX) { OpenMode.SFTP @@ -766,18 +769,25 @@ class SftpConnectDialog : DialogFragment() { ) } - private fun createConnectionSettings() = + // FIXME: username/password may not need urlEncoded during edit mode + private fun createConnectionSettings(edit: Boolean = false) = ConnectionSettings( prefix = getProtocolPrefixFromDropdownSelection(), connectionName = binding.connectionET.text.toString(), hostname = binding.ipET.text.toString(), port = binding.portET.text.toString().toInt(), defaultPath = binding.defaultPathET.text.toString(), - username = binding.usernameET.text.toString(), + username = binding.usernameET.text.toString().urlEncoded(), password = if (true == binding.passwordET.text?.isEmpty()) { - arguments?.getString(ARG_PASSWORD, null) + if (edit) { + requireArguments().getString(ARG_PASSWORD, null)?.run { + PasswordUtil.decryptPassword(AppConfig.getInstance(), this) + } + } else { + requireArguments().getString(ARG_PASSWORD, null) + } } else { - binding.passwordET.text.toString() + binding.passwordET.text.toString().urlEncoded() }, selectedParsedKeyPairName = this.selectedParsedKeyPairName, selectedParsedKeyPair = selectedParsedKeyPair diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialog.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialog.java index 12d673bd0e..50f30fd136 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialog.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialog.java @@ -20,54 +20,81 @@ package com.amaze.filemanager.ui.dialogs; +import static android.util.Base64.URL_SAFE; +import static com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.AT; +import static com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.COLON; +import static com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.SLASH; import static com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX; import static com.amaze.filemanager.utils.SmbUtil.PARAM_DISABLE_IPC_SIGNING_CHECK; +import static java.net.URLDecoder.decode; +import static java.net.URLEncoder.encode; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; -import java.net.URLDecoder; -import java.net.URLEncoder; +import java.security.GeneralSecurityException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; +import com.amaze.filemanager.databinding.SmbDialogBinding; import com.amaze.filemanager.filesystem.smb.CifsContexts; import com.amaze.filemanager.ui.ExtensionsKt; import com.amaze.filemanager.ui.activities.superclasses.BasicActivity; import com.amaze.filemanager.ui.activities.superclasses.ThemedActivity; import com.amaze.filemanager.ui.provider.UtilitiesProvider; import com.amaze.filemanager.utils.EditTextColorStateUtil; +import com.amaze.filemanager.utils.PasswordUtil; import com.amaze.filemanager.utils.SimpleTextWatcher; import com.amaze.filemanager.utils.SmbUtil; import com.amaze.filemanager.utils.Utils; import com.google.android.material.textfield.TextInputLayout; import android.app.Dialog; -import android.app.DialogFragment; import android.content.Context; -import android.content.SharedPreferences; import android.net.UrlQuerySanitizer; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; -import android.view.View; +import android.view.LayoutInflater; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.appcompat.widget.AppCompatCheckBox; import androidx.appcompat.widget.AppCompatEditText; -import androidx.preference.PreferenceManager; +import androidx.fragment.app.DialogFragment; import jcifs.smb.SmbFile; +import kotlin.text.Charsets; public class SmbConnectDialog extends DialogFragment { + // Dialog tag. + public static final String TAG = "smbdialog"; + + public static final String ARG_NAME = "name"; + + public static final String ARG_PATH = "path"; + + public static final String ARG_EDIT = "edit"; + + private static final Logger LOG = LoggerFactory.getLogger(SmbConnectDialog.class); + private UtilitiesProvider utilsProvider; - private final Logger LOG = LoggerFactory.getLogger(SmbConnectDialog.class); + private SmbConnectionListener smbConnectionListener; + + private SmbDialogBinding binding; + private String emptyAddress; + private String emptyName; + private String invalidDomain; + private String invalidUsername; public interface SmbConnectionListener { @@ -76,8 +103,6 @@ public interface SmbConnectionListener { * * @param edit whether we edit existing connection or not * @param name name of connection as appears in navigation drawer - * @param path the full path to the server. Includes an un-encrypted password to support runtime - * loading without reloading stuff from database. * @param encryptedPath the full path to the server. Includes encrypted password to save in * database. Later be decrypted at every boot when we read from db entry. * @param oldname the old name of connection if we're here to edit @@ -86,11 +111,10 @@ public interface SmbConnectionListener { */ void addConnection( boolean edit, - String name, - String path, - String encryptedPath, - String oldname, - String oldPath); + @NonNull String name, + @NonNull String encryptedPath, + @Nullable String oldname, + @Nullable String oldPath); /** * Callback denoting a connection been deleted from dialog @@ -107,9 +131,20 @@ void addConnection( void deleteConnection(String name, String path); } - Context context; - SmbConnectionListener smbConnectionListener; - String emptyAddress, emptyName, invalidDomain, invalidUsername; + @VisibleForTesting + public void setSmbConnectionListener(SmbConnectionListener smbConnectionListener) { + this.smbConnectionListener = smbConnectionListener; + } + + @VisibleForTesting + public SmbConnectionListener getSmbConnectionListener() { + return smbConnectionListener; + } + + @VisibleForTesting + public SmbDialogBinding getBinding() { + return binding; + } @Override public void onCreate(Bundle savedInstanceState) { @@ -117,32 +152,31 @@ public void onCreate(Bundle savedInstanceState) { utilsProvider = ((BasicActivity) getActivity()).getUtilsProvider(); } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - final boolean edit = getArguments().getBoolean("edit", false); - final String path = getArguments().getString("path"); - final String name = getArguments().getString("name"); - context = getActivity(); + final boolean edit = getArguments().getBoolean(ARG_EDIT, false); + final String path = getArguments().getString(ARG_PATH); + final String name = getArguments().getString(ARG_NAME); + Context context = requireActivity(); emptyAddress = getString(R.string.cant_be_empty, getString(R.string.ip)); emptyName = getString(R.string.cant_be_empty, getString(R.string.connection_name)); invalidDomain = getString(R.string.invalid, getString(R.string.domain)); invalidUsername = getString(R.string.invalid, getString(R.string.username).toLowerCase()); - if (getActivity() instanceof SmbConnectionListener) { + if (requireActivity() instanceof SmbConnectionListener && smbConnectionListener == null) { smbConnectionListener = (SmbConnectionListener) getActivity(); } - final SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); final MaterialDialog.Builder ba3 = new MaterialDialog.Builder(context); ba3.title((R.string.smb_connection)); ba3.autoDismiss(false); - final View v2 = getActivity().getLayoutInflater().inflate(R.layout.smb_dialog, null); - final TextInputLayout connectionTIL = v2.findViewById(R.id.connectionTIL); - final TextInputLayout ipTIL = v2.findViewById(R.id.ipTIL); - final TextInputLayout domainTIL = v2.findViewById(R.id.domainTIL); - final TextInputLayout usernameTIL = v2.findViewById(R.id.usernameTIL); - final TextInputLayout passwordTIL = v2.findViewById(R.id.passwordTIL); - final AppCompatEditText conName = v2.findViewById(R.id.connectionET); + binding = SmbDialogBinding.inflate(LayoutInflater.from(context)); + final TextInputLayout connectionTIL = binding.connectionTIL; + final TextInputLayout ipTIL = binding.ipTIL; + final TextInputLayout domainTIL = binding.domainTIL; + final TextInputLayout usernameTIL = binding.usernameTIL; + final TextInputLayout passwordTIL = binding.passwordTIL; + final AppCompatEditText conName = binding.connectionET; ExtensionsKt.makeRequired(connectionTIL); ExtensionsKt.makeRequired(ipTIL); @@ -152,46 +186,46 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { conName.addTextChangedListener( new SimpleTextWatcher() { @Override - public void afterTextChanged(Editable s) { + public void afterTextChanged(@NonNull Editable s) { if (conName.getText().toString().length() == 0) connectionTIL.setError(emptyName); else connectionTIL.setError(""); } }); - final AppCompatEditText ip = v2.findViewById(R.id.ipET); + final AppCompatEditText ip = binding.ipET; ip.addTextChangedListener( new SimpleTextWatcher() { @Override - public void afterTextChanged(Editable s) { + public void afterTextChanged(@NonNull Editable s) { if (ip.getText().toString().length() == 0) ipTIL.setError(emptyAddress); else ipTIL.setError(""); } }); - final AppCompatEditText share = v2.findViewById(R.id.shareET); - final AppCompatEditText domain = v2.findViewById(R.id.domainET); + final AppCompatEditText share = binding.shareET; + final AppCompatEditText domain = binding.domainET; domain.addTextChangedListener( new SimpleTextWatcher() { @Override - public void afterTextChanged(Editable s) { + public void afterTextChanged(@NonNull Editable s) { if (domain.getText().toString().contains(";")) domainTIL.setError(invalidDomain); else domainTIL.setError(""); } }); - final AppCompatEditText user = v2.findViewById(R.id.usernameET); + final AppCompatEditText user = binding.usernameET; user.addTextChangedListener( new SimpleTextWatcher() { @Override - public void afterTextChanged(Editable s) { - if (user.getText().toString().contains(":")) usernameTIL.setError(invalidUsername); + public void afterTextChanged(@NonNull Editable s) { + if (user.getText().toString().contains(String.valueOf(COLON))) + usernameTIL.setError(invalidUsername); else usernameTIL.setError(""); } }); int accentColor = ((ThemedActivity) getActivity()).getAccent(); - final AppCompatEditText pass = v2.findViewById(R.id.passwordET); - final AppCompatCheckBox chkSmbAnonymous = v2.findViewById(R.id.chkSmbAnonymous); - final AppCompatCheckBox chkSmbDisableIpcSignature = - v2.findViewById(R.id.chkSmbDisableIpcSignature); - TextView help = v2.findViewById(R.id.wanthelp); + final AppCompatEditText pass = binding.passwordET; + final AppCompatCheckBox chkSmbAnonymous = binding.chkSmbAnonymous; + final AppCompatCheckBox chkSmbDisableIpcSignature = binding.chkSmbDisableIpcSignature; + TextView help = binding.wanthelp; EditTextColorStateUtil.setTint(context, conName, accentColor); EditTextColorStateUtil.setTint(context, user, accentColor); @@ -205,7 +239,7 @@ public void afterTextChanged(Editable s) { }); chkSmbAnonymous.setOnClickListener( - view -> { + v -> { if (chkSmbAnonymous.isChecked()) { user.setEnabled(false); pass.setEnabled(false); @@ -227,12 +261,20 @@ public void afterTextChanged(Editable s) { URL a = new URL(path); String userinfo = a.getUserInfo(); if (userinfo != null) { - String inf = URLDecoder.decode(userinfo, "UTF-8"); + String inf = decode(userinfo, Charsets.UTF_8.name()); int domainDelim = !inf.contains(";") ? 0 : inf.indexOf(';'); domainp = inf.substring(0, domainDelim); if (domainp != null && domainp.length() > 0) inf = inf.substring(domainDelim + 1); userp = inf.substring(0, inf.indexOf(":")); - passp = inf.substring(inf.indexOf(":") + 1, inf.length()); + try { + passp = + PasswordUtil.INSTANCE.decryptPassword( + context, inf.substring(inf.indexOf(COLON) + 1), URL_SAFE); + passp = decode(passp, Charsets.UTF_8.name()); + } catch (GeneralSecurityException | IOException e) { + LOG.warn("Error decrypting password", e); + passp = ""; + } domain.setText(domainp); user.setText(userp); pass.setText(passp); @@ -264,15 +306,15 @@ public void afterTextChanged(Editable s) { conName.requestFocus(); } - ba3.customView(v2, true); - ba3.theme(utilsProvider.getAppTheme().getMaterialDialogTheme(getContext())); - ba3.neutralText(R.string.cancel); - ba3.positiveText(R.string.create); + ba3.customView(binding.getRoot(), true); + ba3.theme(utilsProvider.getAppTheme().getMaterialDialogTheme(context)); + ba3.neutralText(android.R.string.cancel); + ba3.positiveText(edit ? R.string.update : R.string.create); if (edit) ba3.negativeText(R.string.delete); ba3.positiveColor(accentColor).negativeColor(accentColor).neutralColor(accentColor); ba3.onPositive( (dialog, which) -> { - String s[]; + String[] s; String ipa = ip.getText().toString(); String con_nam = conName.getText().toString(); String sDomain = domain.getText().toString(); @@ -332,8 +374,7 @@ public void afterTextChanged(Editable s) { if (smbConnectionListener != null) { // encrypted path means path with encrypted pass String qs = extraParams.length() > 0 ? extraParams.insert(0, '?').toString() : ""; - smbConnectionListener.addConnection( - edit, s[0], smbFile.getPath() + qs, s[1] + qs, name, path); + smbConnectionListener.addConnection(edit, s[0], s[1] + qs, name, path); } dismiss(); }); @@ -350,6 +391,7 @@ public void afterTextChanged(Editable s) { return ba3.build(); } + // Begin URL building, hence will need to URL encode credentials here, to begin with. private SmbFile createSMBPath(String[] auth, boolean anonymous, boolean disableIpcSignCheck) { try { String yourPeerIP = auth[0]; @@ -357,22 +399,19 @@ private SmbFile createSMBPath(String[] auth, boolean anonymous, boolean disableI String share = auth[4]; StringBuilder sb = new StringBuilder(SMB_URI_PREFIX); - if (!android.text.TextUtils.isEmpty(domain)) - sb.append(URLEncoder.encode(domain + ";", "UTF-8")); + if (!TextUtils.isEmpty(domain)) sb.append(encode(domain + ";", Charsets.UTF_8.name())); if (!anonymous) - sb.append(URLEncoder.encode(auth[1], "UTF-8")) - .append(":") - .append(URLEncoder.encode(auth[2], "UTF-8")) - .append("@"); - sb.append(yourPeerIP).append("/"); + sb.append(encode(auth[1], Charsets.UTF_8.name())) + .append(COLON) + .append(encode(auth[2], Charsets.UTF_8.name())) + .append(AT); + sb.append(yourPeerIP).append(SLASH); if (!TextUtils.isEmpty(share)) { - sb.append(share).append("/"); + sb.append(share).append(SLASH); } - SmbFile smbFile = - new SmbFile( - sb.toString(), - CifsContexts.createWithDisableIpcSigningCheck(sb.toString(), disableIpcSignCheck)); - return smbFile; + return new SmbFile( + sb.toString(), + CifsContexts.createWithDisableIpcSigningCheck(sb.toString(), disableIpcSignCheck)); } catch (MalformedURLException e) { LOG.warn("failed to load smb path", e); } catch (UnsupportedEncodingException | IllegalArgumentException e) { diff --git a/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt index 72c8bfef1d..35ae7bd5b9 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt @@ -23,7 +23,7 @@ package com.amaze.filemanager.utils import android.content.Context import android.net.Uri import android.text.TextUtils -import android.util.Log +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.fileoperations.filesystem.DOESNT_EXIST import com.amaze.filemanager.fileoperations.filesystem.WRITABLE_ON_REMOTE import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo @@ -35,6 +35,7 @@ import io.reactivex.schedulers.Schedulers import jcifs.smb.NtlmPasswordAuthenticator import jcifs.smb.SmbException import jcifs.smb.SmbFile +import org.slf4j.LoggerFactory import java.net.MalformedURLException /** @@ -45,14 +46,16 @@ import java.net.MalformedURLException */ object SmbUtil { - private const val TAG = "SmbUtil" + @JvmStatic + private val LOG = LoggerFactory.getLogger(SmbUtil::class.java) + const val PARAM_DISABLE_IPC_SIGNING_CHECK = "disableIpcSigningCheck" /** Parse path to decrypt smb password */ @JvmStatic fun getSmbDecryptedPath(context: Context, path: String): String { return buildPath(path, withPassword = { - PasswordUtil.decryptPassword(context, it) + PasswordUtil.decryptPassword(context, it.urlDecoded()) }) } @@ -64,6 +67,8 @@ object SmbUtil { }) } + // At this point, credential is URL encoded to be safe from special chars. + // No need to call URLEncoder.encode() again private fun buildPath(path: String, withPassword: (String) -> String?): String { if (!(path.contains(COLON) && path.contains(AT))) { // smb path doesn't have any credentials @@ -72,11 +77,11 @@ object SmbUtil { val buffer = StringBuilder() NetCopyConnectionInfo(path).let { connectionInfo -> buffer.append(connectionInfo.prefix).append( - if (connectionInfo.username.isEmpty()) "" else connectionInfo.username.urlEncoded() + connectionInfo.username.ifEmpty { "" } ) if (false == connectionInfo.password?.isEmpty()) { val password = withPassword.invoke(connectionInfo.password) - buffer.append(COLON).append(password) + buffer.append(COLON).append(password?.replace("\n", "")) } buffer.append(AT).append(connectionInfo.host) if (connectionInfo.port > 0) { @@ -95,7 +100,7 @@ object SmbUtil { @JvmStatic @Throws(MalformedURLException::class) fun create(path: String): SmbFile { - val uri = Uri.parse(path) + val uri = Uri.parse(getSmbDecryptedPath(AppConfig.getInstance(), path)) val disableIpcSigningCheck = uri.getQueryParameter( PARAM_DISABLE_IPC_SIGNING_CHECK ).toBoolean() @@ -162,10 +167,10 @@ object SmbUtil { val smbFile = create(path) if (!smbFile.exists() || !smbFile.isDirectory) return@fromCallable DOESNT_EXIST } catch (e: SmbException) { - Log.w(TAG, "Error checking folder existence, assuming not exist", e) + LOG.warn("Error checking folder existence, assuming not exist", e) return@fromCallable DOESNT_EXIST } catch (e: MalformedURLException) { - Log.w(TAG, "Error checking folder existence, assuming not exist", e) + LOG.warn("Error checking folder existence, assuming not exist", e) return@fromCallable DOESNT_EXIST } WRITABLE_ON_REMOTE diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt index e5bc68ad1f..9a208729fa 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt @@ -104,8 +104,13 @@ class UtilsHandlerTest { saveToDatabase(o) val verify = smbList assertEquals(1, verify.size) - // UtilsHandler.getSmbList() will decrypt password when return - assertEquals(path, verify[0][1]) + assertEquals( + SmbUtil.getSmbEncryptedPath( + AppConfig.getInstance(), + path + ), + verify[0][1] + ) } utilitiesDatabase.run { val verify = smbEntryDao().list().blockingGet() diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt index 4993edce5e..eba4d8dbde 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt @@ -40,6 +40,7 @@ import kotlin.random.Random /* ktlint-disable max-line-length */ @RunWith(AndroidJUnit4::class) @Config(shadows = [ShadowMultiDex::class], sdk = [KITKAT, P, Build.VERSION_CODES.R]) +@Suppress("StringLiteralDuplication") class HybridFileTest { /** @@ -225,5 +226,47 @@ class HybridFileTest { ) assertEquals("down the pipe", file.getName(AppConfig.getInstance())) } + + /** + * Test [HybridFile.sanitizePathAsNecessary]. + */ + @Test + fun testSanitizePathAsNecessary() { + assertEquals( + "ftp://user:password@127.0.0.1:22222/multiple/levels/down/the/pipe", + HybridFile( + OpenMode.FTP, + "ftp://user:password@127.0.0.1:22222//multiple///levels////down////the/pipe" + ).path + ) + assertEquals( + "ssh://user@127.0.0.1/multiple/levels/down/the/pipe", + HybridFile( + OpenMode.SFTP, + "ssh://user@127.0.0.1//multiple///levels////down////the/pipe" + ).path + ) + assertEquals( + "ssh://user@127.0.0.1/multiple/levels/down/the/pipe", + HybridFile( + OpenMode.SFTP, + "ssh://user@127.0.0.1/multiple/levels/down/the/pipe" + ).path + ) + assertEquals( + "smb://127.0.0.1/legacy?disableIpcSigningCheck=true", + HybridFile( + OpenMode.SMB, + "smb://127.0.0.1/legacy?disableIpcSigningCheck=true" + ).path + ) + assertEquals( + "smb://127.0.0.1/legacy/again/try/duplicate/folder?disableIpcSigningCheck=true", + HybridFile( + OpenMode.SMB, + "smb://127.0.0.1/legacy//again/try/duplicate/////folder?disableIpcSigningCheck=true" + ).path + ) + } } /* ktlint-enable max-line-length */ diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt index df2e40bf19..3ba38ab049 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt @@ -28,7 +28,6 @@ import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.getConnection import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.shutdown -import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils.encryptFtpPathAsNecessary import com.amaze.filemanager.filesystem.ssh.test.TestUtils import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowPasswordUtil @@ -207,20 +206,20 @@ class NetCopyClientConnectionPoolFtpTest { private fun doRunTest(validUsername: String, validPassword: String) { val encodedUsername = encode(validUsername, UTF_8.name()) val encodedPassword = encode(validPassword, UTF_8.name()) + val encryptedPassword = PasswordUtil.encryptPassword( + AppConfig.getInstance(), + encodedPassword + )?.replace("\n", "") val mock = createFTPClient(validUsername, validPassword) TestUtils.saveFtpConnectionSettings(validUsername, validPassword) assertNotNull( getConnection( - encryptFtpPathAsNecessary( - "ftp://$encodedUsername:$encodedPassword@127.0.0.1:22222" - ) + "ftp://$encodedUsername:$encryptedPassword@127.0.0.1:22222" ) ) assertNull( getConnection( - encryptFtpPathAsNecessary( - "ftp://$encodedInvalidUsername:$encodedInvalidPassword@127.0.0.1:22222" - ) + "ftp://$encodedInvalidUsername:$encodedInvalidPassword@127.0.0.1:22222" ) ) verify(mock, atLeastOnce()).connectTimeout = NetCopyClientConnectionPool.CONNECT_TIMEOUT diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt index 774ce3fef0..279b660215 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyConnectionInfoTest.kt @@ -20,9 +20,6 @@ package com.amaze.filemanager.filesystem.ftp -import android.os.Build.VERSION_CODES -import android.os.Build.VERSION_CODES.KITKAT -import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowPasswordUtil @@ -31,12 +28,12 @@ import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config +import java.net.URLDecoder.decode import java.net.URLEncoder.encode /* ktlint-disable max-line-length */ @RunWith(AndroidJUnit4::class) @Config( - sdk = [KITKAT, P, VERSION_CODES.R], shadows = [ShadowPasswordUtil::class, ShadowMultiDex::class] ) @Suppress("StringLiteralDuplication") @@ -46,8 +43,15 @@ class NetCopyConnectionInfoTest { * Test unsupported URL prefixes should throw IllegalArgumentException. */ @Test(expected = IllegalArgumentException::class) - fun testUnsupportedPrefix() { + fun testUnsupportedHttpPrefix() { NetCopyConnectionInfo("http://github.com") + } + + /** + * Test to verify SMB prefix is supported. + */ + @Test + fun testSmbPrefixSupported() { NetCopyConnectionInfo("smb://user:pass@127.0.0.1") } @@ -276,7 +280,8 @@ class NetCopyConnectionInfoTest { ).run { assertEquals("ftp://", prefix) assertEquals("testuser", username) - assertEquals("testP@##word", password) + assertEquals("testP%40%23%23word", password) + assertEquals("testP@##word", decode("testP@##word", Charsets.UTF_8.name())) assertEquals("127.0.0.1", host) assertEquals(22222, port) assertNull(defaultPath) @@ -298,8 +303,33 @@ class NetCopyConnectionInfoTest { assertEquals("ssh://", this.prefix) assertEquals("127.0.0.1", this.host) assertEquals(32, this.port) - assertEquals(username, this.username) - assertEquals(password, this.password) + assertEquals(username, decode(this.username, Charsets.UTF_8.name())) + assertEquals(password, decode(this.password, Charsets.UTF_8.name())) + } + } + + /** + * Test parsing cleaning up duplicated slashes. + */ + @Test + fun testParseDuplicatedSlashes() { + NetCopyConnectionInfo("smb://user:pass@127.0.0.1/test/1/2/3/4.txt").run { + assertEquals("smb://user@127.0.0.1/test/1/2/3", this.toString()) + assertEquals("4.txt", this.lastPathSegment()) + } + NetCopyConnectionInfo("smb://user:pass@127.0.0.1/test//1///2////3/4.txt").run { + assertEquals("smb://user@127.0.0.1/test/1/2/3", this.toString()) + assertEquals("4.txt", this.lastPathSegment()) + } + NetCopyConnectionInfo("ssh://user:pass@127.0.0.1/a/b/c/d/e").run { + assertEquals("ssh://user@127.0.0.1/a/b/c/d/e", this.toString()) + assertNotNull(this.lastPathSegment()) + } + NetCopyConnectionInfo( + "ftp://127.0.0.1////a/bunch///of///slash//folders////////////test.log" + ).run { +// assertEquals("ftp://127.0.0.1/a/bunch/of/slash/folders", this.toString()) + assertEquals("test.log", this.lastPathSegment()) } } } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt index 18008f0466..7d5bb60441 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt @@ -30,6 +30,7 @@ import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_ import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.getConnection import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.shutdown import com.amaze.filemanager.filesystem.ssh.test.TestKeyProvider +import com.amaze.filemanager.filesystem.ssh.test.TestUtils import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowPasswordUtil import com.amaze.filemanager.utils.PasswordUtil @@ -54,7 +55,9 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config import java.io.IOException import java.net.BindException +import java.net.URLEncoder.encode import java.nio.file.Paths +import kotlin.text.Charsets.UTF_8 /** * Base class for all SSH server related tests. @@ -67,7 +70,7 @@ import java.nio.file.Paths abstract class AbstractSftpServerTest { protected var encryptedPassword: String? = - PasswordUtil.encryptPassword(AppConfig.getInstance(), PASSWORD) + PasswordUtil.encryptPassword(AppConfig.getInstance(), PASSWORD)?.replace("\n", "") protected var serverPort = 0 private lateinit var server: SshServer @@ -84,6 +87,13 @@ abstract class AbstractSftpServerTest { 64000 ) prepareSshConnection() + TestUtils.saveSshConnectionSettings( + hostKeyPair = hostKeyProvider.keyPair, + validUsername = encode(USERNAME, UTF_8.name()), + validPassword = encryptedPassword, + privateKey = null, + port = 64000 + ) } /** @@ -106,7 +116,7 @@ abstract class AbstractSftpServerTest { serverPort, hostFingerprint, USERNAME, - PasswordUtil.encryptPassword(AppConfig.getInstance(), PASSWORD), + PasswordUtil.encryptPassword(AppConfig.getInstance(), PASSWORD)?.replace("\n", ""), null ) } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt index 013a0f09d1..858618e436 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt @@ -40,6 +40,7 @@ import java.io.File import java.io.FileOutputStream import java.nio.file.Files import java.nio.file.Paths +import java.util.concurrent.TimeUnit /** * Test listing files on SSH server. @@ -153,7 +154,7 @@ class FilesOnSshdTest : AbstractSftpServerTest() { } } ) - await().until { result.size == 2 } + await().atMost(90, TimeUnit.SECONDS).until { result.size == 2 } assertThat>( result, Matchers.hasItems("test+file.bin", "D:") diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt index 285fed9b36..7d8151c47e 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt @@ -317,23 +317,25 @@ class NetCopyClientConnectionPoolSshTest { private fun doRunTest(validUsername: String, validPassword: String, subPath: String? = null) { val encodedUsername = encode(validUsername, UTF_8.name()) val encodedPassword = encode(validPassword, UTF_8.name()) + val encryptedPassword = PasswordUtil.encryptPassword( + AppConfig.getInstance(), + encodedPassword + )?.replace("\n", "") val mock = createSshServer(validUsername, validPassword) TestUtils.saveSshConnectionSettings( hostKeyPair, - validUsername, - validPassword, + encodedUsername, + encryptedPassword, null, subPath ) assertNotNull( getConnection( - encryptFtpPathAsNecessary( - if (subPath.isNullOrEmpty()) { - "ssh://$encodedUsername:$encodedPassword@$HOST:$PORT" - } else { - "ssh://$encodedUsername:$encodedPassword@$HOST:$PORT$subPath" - } - ) + if (subPath.isNullOrEmpty()) { + "ssh://$encodedUsername:$encryptedPassword@$HOST:$PORT" + } else { + "ssh://$encodedUsername:$encryptedPassword@$HOST:$PORT$subPath" + } ) ) assertNull( @@ -370,7 +372,7 @@ class NetCopyClientConnectionPoolSshTest { val mock = createSshServer(validUsername, null) TestUtils.saveSshConnectionSettings( hostKeyPair, - validUsername, + encodedUsername, null, validPrivateKey, subPath diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/test/TestUtils.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/test/TestUtils.kt index 1bb2cc2b5f..e816db9acc 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/test/TestUtils.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/test/TestUtils.kt @@ -35,12 +35,10 @@ import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.json.JSONObject import org.robolectric.Shadows import java.io.StringWriter -import java.net.URLEncoder.encode import java.security.KeyPair import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.SecureRandom -import kotlin.text.Charsets.UTF_8 /** * Test support util methods. @@ -74,8 +72,8 @@ object TestUtils { } ) if (validUsername != "" && validPassword != "") { - fullUri.append(encode(validUsername, UTF_8.name())) - fullUri.append(':').append(encode(validPassword, UTF_8.name())).append("@") + fullUri.append(validUsername) + fullUri.append(':').append(validPassword).append("@") } fullUri.append("${NetCopyClientConnectionPoolFtpTest.HOST}:$port") @@ -100,7 +98,8 @@ object TestUtils { validUsername: String, validPassword: String?, privateKey: PrivateKey?, - subpath: String? = null + subpath: String? = null, + port: Int = NetCopyClientConnectionPoolSshTest.PORT ) { val utilsHandler = AppConfig.getInstance().utilsHandler var privateKeyContents: String? = null @@ -113,10 +112,10 @@ object TestUtils { privateKeyContents = writer.toString() } val fullUri: StringBuilder = StringBuilder() - .append(SSH_URI_PREFIX).append(encode(validUsername, UTF_8.name())) - if (validPassword != null) fullUri.append(':').append(encode(validPassword, UTF_8.name())) + .append(SSH_URI_PREFIX).append(validUsername) + if (validPassword != null) fullUri.append(':').append(validPassword) fullUri.append( - "@${NetCopyClientConnectionPoolSshTest.HOST}:${NetCopyClientConnectionPoolSshTest.PORT}" + "@${NetCopyClientConnectionPoolSshTest.HOST}:$port" ) if (true == subpath?.isNotEmpty()) { @@ -126,7 +125,7 @@ object TestUtils { if (validPassword != null) utilsHandler.saveToDatabase( OperationData( UtilsHandler.Operation.SFTP, - encryptFtpPathAsNecessary(fullUri.toString()), + fullUri.toString(), "Test", SecurityUtils.getFingerprint(hostKeyPair.public), null, diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt new file mode 100644 index 0000000000..aae1333837 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.ui.activities + +import android.Manifest +import android.content.Context +import android.os.Build +import android.os.Build.VERSION_CODES +import android.os.Build.VERSION_CODES.KITKAT +import android.os.Build.VERSION_CODES.P +import android.os.storage.StorageManager +import androidx.annotation.NonNull +import androidx.annotation.RequiresApi +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.shadows.jcifs.smb.ShadowSmbFile +import com.amaze.filemanager.test.ShadowPasswordUtil +import com.amaze.filemanager.test.TestUtils +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.runner.RunWith +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.robolectric.shadows.ShadowSQLiteConnection +import org.robolectric.shadows.ShadowStorageManager + +/** + * Base class for all [MainActivity] related tests. + */ +@RunWith(AndroidJUnit4::class) +@Config( + sdk = [KITKAT, P, VERSION_CODES.R], + shadows = [ + ShadowMultiDex::class, + ShadowStorageManager::class, + ShadowPasswordUtil::class, + ShadowSmbFile::class + ] +) +/* + * Need to make LooperMode PAUSED and flush the main looper before activity can show up. + * @see {@link LooperMode.Mode.PAUSED} + * @see {@link StackOverflow discussion} + */ +@LooperMode(LooperMode.Mode.PAUSED) +abstract class AbstractMainActivityTestBase { + + @Rule + @NonNull + @JvmField + @RequiresApi(Build.VERSION_CODES.R) + val allFilesPermissionRule = + GrantPermissionRule.grant(Manifest.permission.MANAGE_EXTERNAL_STORAGE) + + /** + * Setups before test. + */ + @Before + open fun setUp() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) TestUtils.initializeInternalStorage() + RxJavaPlugins.reset() + RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.reset() + RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } + ShadowSQLiteConnection.reset() + } + + /** + * Post test cleanups. + */ + @After + open fun tearDown() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Shadows.shadowOf( + ApplicationProvider.getApplicationContext().getSystemService( + StorageManager::class.java + ) + ).resetStorageVolumeList() + } + } +} 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 deleted file mode 100644 index 20f1b4aa2a..0000000000 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.java +++ /dev/null @@ -1,308 +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.ui.activities; - -import static android.os.Build.VERSION_CODES.KITKAT; -import static android.os.Build.VERSION_CODES.N; -import static android.os.Build.VERSION_CODES.P; -import static androidx.test.core.app.ActivityScenario.launch; -import static org.awaitility.Awaitility.await; -import static org.junit.Assert.assertEquals; -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; -import java.security.GeneralSecurityException; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -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.ftp.NetCopyClientUtils; -import com.amaze.filemanager.shadows.ShadowMultiDex; -import com.amaze.filemanager.shadows.jcifs.smb.ShadowSmbFile; -import com.amaze.filemanager.test.ShadowPasswordUtil; -import com.amaze.filemanager.test.TestUtils; -import com.amaze.filemanager.ui.dialogs.SftpConnectDialog; -import com.amaze.filemanager.utils.PasswordUtil; -import com.amaze.filemanager.utils.SmbUtil; - -import android.Manifest; -import android.os.Build; -import android.os.Bundle; -import android.os.storage.StorageManager; -import android.util.Base64; - -import androidx.annotation.RequiresApi; -import androidx.lifecycle.Lifecycle; -import androidx.test.core.app.ActivityScenario; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.rule.GrantPermissionRule; - -import io.reactivex.android.plugins.RxAndroidPlugins; -import io.reactivex.plugins.RxJavaPlugins; -import io.reactivex.schedulers.Schedulers; - -@RunWith(AndroidJUnit4.class) -@Config( - sdk = {KITKAT, P, Build.VERSION_CODES.R}, - shadows = { - ShadowMultiDex.class, - ShadowStorageManager.class, - ShadowPasswordUtil.class, - ShadowSmbFile.class - }) -/* - * Need to make LooperMode PAUSED and flush the main looper before activity can show up. - * @see {@link LooperMode.Mode.PAUSED} - * @see {@link StackOverflow discussion} - */ -@LooperMode(LooperMode.Mode.PAUSED) -public class MainActivityTest { - - private static final String[] BUNDLE_KEYS = { - "address", "port", "keypairName", "name", "username", "password", "edit", "defaultPath" - }; - - @Rule - @RequiresApi(Build.VERSION_CODES.R) - public final GrantPermissionRule allFilesPermissionRule = - GrantPermissionRule.grant(Manifest.permission.MANAGE_EXTERNAL_STORAGE); - - private MockedConstruction mc; - - @Before - public void setUp() { - if (Build.VERSION.SDK_INT >= N) TestUtils.initializeInternalStorage(); - RxJavaPlugins.reset(); - 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 - public void tearDown() { - if (Build.VERSION.SDK_INT >= N) - shadowOf(ApplicationProvider.getApplicationContext().getSystemService(StorageManager.class)) - .resetStorageVolumeList(); - - mc.close(); - } - - @Test - public void testInvokeSftpConnectionDialog() throws GeneralSecurityException, IOException { - - 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 = - NetCopyClientUtils.INSTANCE.encryptFtpPathAsNecessary("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", - PasswordUtil.INSTANCE - .encryptPassword(AppConfig.getInstance(), "12345678", Base64.URL_SAFE) - .replace("\n", "")); - - testOpenSftpConnectDialog(uri, verify); - } - - @Test - public void testInvokeSftpConnectionDialogWithPasswordAndDefaultPath() - throws GeneralSecurityException, IOException { - String uri = - NetCopyClientUtils.INSTANCE.encryptFtpPathAsNecessary( - "ssh://root:12345678@127.0.0.1:22/data/incoming"); - - 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("defaultPath", "/data/incoming"); - verify.putString( - "password", - PasswordUtil.INSTANCE - .encryptPassword(AppConfig.getInstance(), "12345678", Base64.URL_SAFE) - .replace("\n", "")); - - testOpenSftpConnectDialog(uri, verify); - } - - @Test - public void testInvokeSftpConnectionDialogWithPasswordAndEncodedDefaultPath() - throws GeneralSecurityException, IOException { - String uri = - NetCopyClientUtils.INSTANCE.encryptFtpPathAsNecessary( - "ssh://root:12345678@127.0.0.1:22/Users/TranceLove/My+Documents/%7BReference%7D%20Zobius%20Facro%20%24%24%20%23RFII1"); - - 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( - "defaultPath", "/Users/TranceLove/My Documents/{Reference} Zobius Facro $$ #RFII1"); - verify.putString( - "password", - PasswordUtil.INSTANCE - .encryptPassword(AppConfig.getInstance(), "12345678", Base64.URL_SAFE) - .replace("\n", "")); - - testOpenSftpConnectDialog(uri, verify); - } - - private void testOpenSftpConnectDialog(String uri, Bundle verify) - throws GeneralSecurityException, IOException { - 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", NetCopyClientUtils.INSTANCE.encryptFtpPathAsNecessary(uri), true); - assertEquals(1, mc.constructed().size()); - SftpConnectDialog mocked = mc.constructed().get(0); - await().atMost(999, TimeUnit.SECONDS).until(() -> mocked.getArguments() != null); - for (String key : BUNDLE_KEYS) { - if (mocked.getArguments().get(key) != null) { - if (!key.equals("password")) { - assertEquals(verify.get(key), mocked.getArguments().get(key)); - } else { - assertEquals( - verify.get(key), - PasswordUtil.INSTANCE.decryptPassword( - ApplicationProvider.getApplicationContext(), - (String) mocked.getArguments().get(key), - Base64.URL_SAFE)); - } - } - } - } - - @Test - public void testUpdateSmbExceptionShouldNotThrowNPE() { - ActivityScenario scenario = launch(MainActivity.class); - - ShadowLooper.idleMainLooper(); - - scenario.moveToState(Lifecycle.State.STARTED); - - scenario.onActivity( - activity -> { - String path = "smb://root:toor@192.168.1.1"; - String oldName = "SMB connection"; - String newName = "root@192.168.1.1"; - try { - - activity.addConnection( - false, - oldName, - path, - SmbUtil.getSmbEncryptedPath(ApplicationProvider.getApplicationContext(), path), - null, - null); - activity.addConnection( - true, - newName, - path, - SmbUtil.getSmbEncryptedPath(ApplicationProvider.getApplicationContext(), path), - oldName, - path); - - ShadowLooper.idleMainLooper(); - - await() - .atMost(5, TimeUnit.SECONDS) - .until(() -> AppConfig.getInstance().getUtilsHandler().getSmbList().size() > 0); - await() - .atMost(5, TimeUnit.SECONDS) - .until( - () -> - AppConfig.getInstance() - .getUtilsHandler() - .getSmbList() - .get(0)[0] - .equals(newName)); - List verify = AppConfig.getInstance().getUtilsHandler().getSmbList(); - assertEquals(1, verify.size()); - String[] entry = verify.get(0); - assertEquals(path, entry[1]); - - } finally { - scenario.moveToState(Lifecycle.State.DESTROYED); - scenario.close(); - } - }); - } -} diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.kt new file mode 100644 index 0000000000..3743edfbca --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.kt @@ -0,0 +1,99 @@ +/* + * 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.ui.activities + +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.utils.SmbUtil.getSmbDecryptedPath +import com.amaze.filemanager.utils.SmbUtil.getSmbEncryptedPath +import org.awaitility.Awaitility.await +import org.junit.Assert.assertEquals +import org.junit.Test +import org.robolectric.shadows.ShadowLooper +import java.util.concurrent.TimeUnit + +/** + * Test [MainActivity]. + */ +@Suppress("StringLiteralDuplication") +class MainActivityTest : AbstractMainActivityTestBase() { + + /** + * Test update SMB connection should never throw [NullPointerException] i.e. the correct + * connection is updated. + */ + @Test + fun testUpdateSmbExceptionShouldNotThrowNPE() { + val scenario = ActivityScenario.launch( + MainActivity::class.java + ) + ShadowLooper.idleMainLooper() + scenario.moveToState(Lifecycle.State.STARTED) + scenario.onActivity { activity: MainActivity -> + val path = "smb://root:toor@192.168.1.1" + val encryptedPath = getSmbEncryptedPath( + ApplicationProvider.getApplicationContext(), + path + ) + val oldName = "SMB connection" + val newName = "root@192.168.1.1" + try { + activity.addConnection( + false, + oldName, + encryptedPath, + null, + null + ) + activity.addConnection( + true, + newName, + encryptedPath, + oldName, + encryptedPath + ) + ShadowLooper.idleMainLooper() + await() + .atMost(10, TimeUnit.SECONDS) + .until { AppConfig.getInstance().utilsHandler.smbList.size > 0 } + await() + .atMost(10, TimeUnit.SECONDS) + .until { + ( + AppConfig.getInstance() + .utilsHandler + .smbList[0][0] + == newName + ) + } + val verify: List> = AppConfig.getInstance().utilsHandler.smbList + assertEquals(1, verify.size.toLong()) + val entry = verify[0] + assertEquals(path, getSmbDecryptedPath(AppConfig.getInstance(), entry[1])) + } finally { + scenario.moveToState(Lifecycle.State.DESTROYED) + scenario.close() + } + } + } +} diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogTest.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogTest.kt new file mode 100644 index 0000000000..fc8db02b62 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialogTest.kt @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.ui.dialogs + +import android.os.Bundle +import android.util.Base64 +import androidx.test.core.app.ApplicationProvider +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.database.UtilsHandler +import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils +import com.amaze.filemanager.ui.activities.AbstractMainActivityTestBase +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.utils.PasswordUtil +import org.awaitility.Awaitility.await +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.MockedConstruction +import org.mockito.Mockito.mock +import org.mockito.Mockito.mockConstruction +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.doCallRealMethod +import org.robolectric.util.ReflectionHelpers +import java.io.IOException +import java.security.GeneralSecurityException +import java.util.concurrent.TimeUnit + +@Suppress("StringLiteralDuplication") +class SftpConnectDialogTest : AbstractMainActivityTestBase() { + + private lateinit var mc: MockedConstruction + + /** + * Setups before test. + */ + @Before + override fun setUp() { + super.setUp() + mc = mockConstruction( + SftpConnectDialog::class.java + ) { mock: SftpConnectDialog, _: MockedConstruction.Context? -> + doCallRealMethod().`when`(mock).arguments = any() + `when`(mock.arguments).thenCallRealMethod() + } + } + + /** + * Post test cleanups. + */ + @After + override fun tearDown() { + super.tearDown() + mc.close() + } + + /** + * Test invoke [SftpConnectDialog] with arguments including keypair name. + */ + @Test + fun testInvokeSftpConnectionDialog() { + val verify = 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 invoke [SftpConnectDialog] with arguments including password. + */ + @Test + fun testInvokeSftpConnectionDialogWithPassword() { + val uri = NetCopyClientUtils.encryptFtpPathAsNecessary("ssh://root:12345678@127.0.0.1:22") + val verify = 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", + PasswordUtil.encryptPassword(AppConfig.getInstance(), "12345678", Base64.URL_SAFE) + ?.replace("\n", "") + ) + testOpenSftpConnectDialog(uri, verify) + } + + /** + * Test invoke [SftpConnectDialog] with arguments including password and default path. + */ + @Test + fun testInvokeSftpConnectionDialogWithPasswordAndDefaultPath() { + val uri = NetCopyClientUtils.encryptFtpPathAsNecessary( + "ssh://root:12345678@127.0.0.1:22/data/incoming" + ) + val verify = 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("defaultPath", "/data/incoming") + verify.putString( + "password", + PasswordUtil.encryptPassword(AppConfig.getInstance(), "12345678", Base64.URL_SAFE) + ?.replace("\n", "") + ) + testOpenSftpConnectDialog(uri, verify) + } + + /** + * Test invoke [SftpConnectDialog] with arguments including password and URL encoded path. + */ + @Test + @Throws(GeneralSecurityException::class, IOException::class) + fun testInvokeSftpConnectionDialogWithPasswordAndEncodedDefaultPath() { + /* ktlint-disable max-line-length */ + val uri = NetCopyClientUtils.encryptFtpPathAsNecessary( + "ssh://root:12345678@127.0.0.1:22/Users/TranceLove/My+Documents/%7BReference%7D%20Zobius%20Facro%20%24%24%20%23RFII1" + ) + /* ktlint-enable max-line-length */ + val verify = 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( + "defaultPath", + "/Users/TranceLove/My Documents/{Reference} Zobius Facro $$ #RFII1" + ) + verify.putString( + "password", + PasswordUtil.encryptPassword(AppConfig.getInstance(), "12345678", Base64.URL_SAFE) + ?.replace("\n", "") + ) + testOpenSftpConnectDialog(uri, verify) + } + + @Throws(GeneralSecurityException::class, IOException::class) + private fun testOpenSftpConnectDialog(uri: String, verify: Bundle) { + val activity = mock(MainActivity::class.java) + val utilsHandler = mock(UtilsHandler::class.java) + `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", + NetCopyClientUtils.encryptFtpPathAsNecessary(uri), + true + ) + assertEquals(1, mc.constructed().size) + val mocked = mc.constructed()[0] + await().atMost(10, TimeUnit.SECONDS).until { mocked.arguments != null } + for (key in BUNDLE_KEYS) { + if (mocked.arguments!![key] != null) { + if (key != "password") { + assertEquals(verify[key], mocked.arguments!![key]) + } else { + assertEquals( + verify[key], + PasswordUtil.decryptPassword( + ApplicationProvider.getApplicationContext(), + (mocked.arguments!![key] as String?)!!, + Base64.URL_SAFE + ) + ) + } + } + } + } + + companion object { + @JvmStatic + private val BUNDLE_KEYS = arrayOf( + "address", + "port", + "keypairName", + "name", + "username", + "password", + "edit", + "defaultPath" + ) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialogTest.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialogTest.kt new file mode 100644 index 0000000000..e2d7a4be17 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialogTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.ui.dialogs + +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import com.afollestad.materialdialogs.DialogAction +import com.afollestad.materialdialogs.MaterialDialog +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.ui.activities.AbstractMainActivityTestBase +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.ui.dialogs.SmbConnectDialog.ARG_EDIT +import com.amaze.filemanager.ui.dialogs.SmbConnectDialog.ARG_NAME +import com.amaze.filemanager.ui.dialogs.SmbConnectDialog.ARG_PATH +import com.amaze.filemanager.ui.dialogs.SmbConnectDialog.SmbConnectionListener +import com.amaze.filemanager.utils.SmbUtil +import io.mockk.confirmVerified +import io.mockk.spyk +import io.mockk.verify +import org.junit.Assert.assertTrue +import org.junit.Test +import org.robolectric.shadows.ShadowDialog +import org.robolectric.shadows.ShadowLooper + +/** + * Tests [SmbConnectDialog]. + */ +class SmbConnectDialogTest : AbstractMainActivityTestBase() { + + /** + * Test call to [SmbConnectionListener.addConnection] is encrypted path. + */ + @Test + fun testCallingAddConnectionIsEncryptedPath() { + val listener = spyk() + doTestWithDialog( + listener = listener, + arguments = Bundle().also { + it.putString(ARG_NAME, "") + it.putString(ARG_PATH, "") + it.putBoolean(ARG_EDIT, false) + }, + withDialog = { dialog, materialDialog -> + val encryptedPath = SmbUtil.getSmbEncryptedPath( + AppConfig.getInstance(), + "smb://user:password@127.0.0.1/" + ) + dialog.binding.run { + this.connectionET.setText("SMB Connection Test") + this.usernameET.setText("user") + this.passwordET.setText("password") + this.ipET.setText("127.0.0.1") + } + assertTrue(materialDialog.getActionButton(DialogAction.POSITIVE).performClick()) + verify { + listener.addConnection( + false, + "SMB Connection Test", + encryptedPath, + "", + "" + ) + } + confirmVerified(listener) + } + ) + } + + private fun doTestWithDialog( + arguments: Bundle, + listener: SmbConnectionListener, + withDialog: (SmbConnectDialog, MaterialDialog) -> Unit + ) { + val scenario = ActivityScenario.launch(MainActivity::class.java) + ShadowLooper.idleMainLooper() + scenario.moveToState(Lifecycle.State.STARTED) + scenario.onActivity { activity -> + SmbConnectDialog().run { + this.smbConnectionListener = listener + this.arguments = arguments + this.show(activity.supportFragmentManager, SmbConnectDialog.TAG) + ShadowLooper.runUiThreadTasks() + assertTrue(ShadowDialog.getLatestDialog().isShowing) + withDialog.invoke(this, ShadowDialog.getLatestDialog() as MaterialDialog) + } + scenario.moveToState(Lifecycle.State.DESTROYED) + scenario.close() + } + } +} diff --git a/testShared/src/test/java/com/amaze/filemanager/shadows/jcifs/smb/ShadowSmbFile.java b/testShared/src/test/java/com/amaze/filemanager/shadows/jcifs/smb/ShadowSmbFile.java index f13a2f5ac4..9a2f6a7c7c 100644 --- a/testShared/src/test/java/com/amaze/filemanager/shadows/jcifs/smb/ShadowSmbFile.java +++ b/testShared/src/test/java/com/amaze/filemanager/shadows/jcifs/smb/ShadowSmbFile.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.nio.file.Files; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -49,7 +50,7 @@ public void setFile(File file) { @Implementation public InputStream getInputStream() throws IOException { - return new FileInputStream(file); + return Files.newInputStream(file.toPath()); } @Implementation From 1b2b65d48d85b81ae65941321dcf1750251bef5c Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 2 Mar 2023 21:02:17 +0530 Subject: [PATCH 064/384] fixes #3731 --- build.gradle | 2 +- gradle.properties | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 89dcd9c767..99910398ce 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.3.1' classpath 'com.hiya:jacoco-android:0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "com.project.starter:easylauncher:4.0.0" + classpath "com.project.starter:easylauncher:6.1.0" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/gradle.properties b/gradle.properties index 6bb98664c4..31554fde00 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,3 +18,6 @@ org.gradle.jvmargs=-Xmx4608M -XX:+UseParallelGC # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true + +# https://github.com/usefulness/easylauncher-gradle-plugin/issues/408 +android.disableResourceValidation=true \ No newline at end of file From 8a158e653891b57bce52fd05ed7621e52771fece Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 2 Mar 2023 22:05:43 +0530 Subject: [PATCH 065/384] chore: refractor `FileUtils#shareFiles` --- .../filesystem/files/FileUtils.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java index a137e6e893..69370ef3e5 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java @@ -389,26 +389,28 @@ protected void onPostExecute(String s) { }.execute(paths); } - public static void shareFiles(ArrayList a, Activity c, AppTheme appTheme, int fab_skin) { + public static void shareFiles( + ArrayList files, Activity activity, AppTheme appTheme, int fab_skin) { ArrayList uris = new ArrayList<>(); - boolean b = true; - for (File f : a) { - uris.add(FileProvider.getUriForFile(c, c.getPackageName(), f)); - } + boolean isGenericFileType = false; + + for (File f : files) + uris.add(FileProvider.getUriForFile(activity, activity.getPackageName(), f)); + + String mime = MimeTypes.getMimeType(files.get(0).getPath(), files.get(0).isDirectory()); - String mime = MimeTypes.getMimeType(a.get(0).getPath(), a.get(0).isDirectory()); - if (a.size() > 1) - for (File f : a) { - if (!mime.equals(MimeTypes.getMimeType(f.getPath(), f.isDirectory()))) { - b = false; + if (files.size() > 1) + for (File file : files) + if (!mime.equals(MimeTypes.getMimeType(file.getPath(), file.isDirectory()))) { + isGenericFileType = true; + break; } - } - if (!b || mime == (null)) mime = MimeTypes.ALL_MIME_TYPES; - try { + if (isGenericFileType || mime == null) mime = MimeTypes.ALL_MIME_TYPES; - new ShareTask(c, uris, appTheme, fab_skin).execute(mime); + try { + new ShareTask(activity, uris, appTheme, fab_skin).execute(mime); } catch (Exception e) { LOG.warn("failed to get share files", e); } From db9f84dbac99d18bf453d63fee1ab211b5017aba Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 2 Mar 2023 22:06:28 +0530 Subject: [PATCH 066/384] fixes #3733 --- .../java/com/amaze/filemanager/filesystem/files/FileUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java index 69370ef3e5..7689724997 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java @@ -402,7 +402,8 @@ public static void shareFiles( if (files.size() > 1) for (File file : files) - if (!mime.equals(MimeTypes.getMimeType(file.getPath(), file.isDirectory()))) { + if (mime == null + || !mime.equals(MimeTypes.getMimeType(file.getPath(), file.isDirectory()))) { isGenericFileType = true; break; } From 2f76898eb1bd91c1cc79e53cf4951f773be0f6bd Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 13 Mar 2023 20:26:59 +0530 Subject: [PATCH 067/384] [WIP] simple search --- .../adapters/SearchRecyclerViewAdapter.kt | 89 +++++++++ .../ui/views/appbar/SearchView.java | 128 +++++++++--- app/src/main/res/layout-v21/layout_search.xml | 185 +++++++++++------- .../main/res/layout-w720dp/layout_search.xml | 168 +++++++++------- app/src/main/res/layout/layout_search.xml | 169 +++++++++------- app/src/main/res/layout/search_row_item.xml | 69 +++++++ 6 files changed, 554 insertions(+), 254 deletions(-) create mode 100644 app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt create mode 100644 app/src/main/res/layout/search_row_item.xml diff --git a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt new file mode 100644 index 0000000000..b9e7eefae5 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2014-2023 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.adapters + +import android.text.format.Formatter +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.amaze.filemanager.R +import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.utils.Utils +import java.util.* + +class SearchRecyclerViewAdapter : + ListAdapter( + + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: HybridFileParcelable, + newItem: HybridFileParcelable + ): Boolean { + return oldItem.path == newItem.path && oldItem.name == newItem.name + } + + override fun areContentsTheSame( + oldItem: HybridFileParcelable, + newItem: HybridFileParcelable + ): Boolean { + return oldItem.path == newItem.path && oldItem.name == newItem.name + } + } + ) { + override fun onCreateViewHolder(parent: ViewGroup, type: Int): ViewHolder { + val v: View = LayoutInflater.from(parent.context) + .inflate(R.layout.search_row_item, parent, false) + return ViewHolder(v) + } + + override fun onBindViewHolder(holder: SearchRecyclerViewAdapter.ViewHolder, position: Int) { + val item = getItem(position) + + holder.fileNameTV.text = item.name + + holder.dateTV.text = Utils.getDate(holder.itemView.context, item.date) + + if (!item.isDirectory) { + holder.sizeTV.text = Formatter.formatFileSize(holder.itemView.context, item.size) + } + } + + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + + val fileNameTV: TextView + val dateTV: TextView + val sizeTV: TextView + + init { + + view.setOnClickListener { + } + + fileNameTV = view.findViewById(R.id.searchItemFileNameTV) + dateTV = view.findViewById(R.id.searchItemDateTV) + sizeTV = view.findViewById(R.id.searchItemSizeTV) + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index efb717407c..4285ac71c6 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -22,10 +22,14 @@ import static android.content.Context.INPUT_METHOD_SERVICE; import static android.os.Build.VERSION.SDK_INT; +import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES; import java.util.ArrayList; import com.amaze.filemanager.R; +import com.amaze.filemanager.adapters.SearchRecyclerViewAdapter; +import com.amaze.filemanager.filesystem.HybridFileParcelable; +import com.amaze.filemanager.filesystem.root.ListFilesCommand; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.theme.AppTheme; @@ -37,6 +41,7 @@ import android.animation.Animator; import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PorterDuff; import android.view.ContextThemeWrapper; @@ -49,9 +54,10 @@ import android.widget.TextView; import androidx.appcompat.widget.AppCompatEditText; -import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; +import androidx.core.widget.NestedScrollView; import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; /** * SearchView, a simple view to search @@ -63,16 +69,20 @@ public class SearchView { private final MainActivity mainActivity; private final AppBar appbar; - private final ConstraintLayout searchViewLayout; + private final NestedScrollView searchViewLayout; private final AppCompatEditText searchViewEditText; private final ImageView clearImageView; private final ImageView backImageView; private final TextView recentHintTV; + private final TextView searchResultsHintTV; + private final ChipGroup recentChipGroup; + private final RecyclerView recyclerView; private final SearchListener searchListener; + private final SearchRecyclerViewAdapter searchRecyclerViewAdapter; private boolean enabled = false; @@ -88,10 +98,21 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener backImageView = mainActivity.findViewById(R.id.img_view_back); recentChipGroup = mainActivity.findViewById(R.id.searchRecentItemsChipGroup); recentHintTV = mainActivity.findViewById(R.id.searchRecentHintTV); + searchResultsHintTV = mainActivity.findViewById(R.id.searchResultsHintTV); + recyclerView = mainActivity.findViewById(R.id.searchRecyclerView); initRecentSearches(mainActivity); - clearImageView.setOnClickListener(v -> searchViewEditText.setText("")); + searchRecyclerViewAdapter = new SearchRecyclerViewAdapter(); + recyclerView.setAdapter(searchRecyclerViewAdapter); + + clearRecyclerView(); + + clearImageView.setOnClickListener( + v -> { + searchViewEditText.setText(""); + clearRecyclerView(); + }); backImageView.setOnClickListener(v -> appbar.getSearchView().hideSearchView()); @@ -99,42 +120,77 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener (v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEARCH) { - String s = searchViewEditText.getText().toString(); + String s = searchViewEditText.getText().toString().trim(); - searchListener.onSearch(s); - appbar.getSearchView().hideSearchView(); + search(s); - String preferenceString = - PreferenceManager.getDefaultSharedPreferences(mainActivity) - .getString(PreferencesConstants.PREFERENCE_RECENT_SEARCH_ITEMS, null); + saveRecentPreference(s); - ArrayList recentSearches = - preferenceString != null - ? new Gson() - .fromJson(preferenceString, new TypeToken>() {}.getType()) - : new ArrayList<>(); + return true; + } + return false; + }); - if (s.isEmpty() || recentSearches.contains(s)) return false; + initSearchViewColor(mainActivity); + } - recentSearches.add(s); + private void search(String s) { - if (recentSearches.size() > 5) recentSearches.remove(0); + clearRecyclerView(); - PreferenceManager.getDefaultSharedPreferences(mainActivity) - .edit() - .putString( - PreferencesConstants.PREFERENCE_RECENT_SEARCH_ITEMS, - new Gson().toJson(recentSearches)) - .apply(); + searchResultsHintTV.setVisibility(View.VISIBLE); - initRecentSearches(mainActivity); + ArrayList hybridFileParcelables = new ArrayList<>(); - return true; - } - return false; + boolean showHiddenFiles = + PreferenceManager.getDefaultSharedPreferences(mainActivity) + .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false); + + ListFilesCommand.INSTANCE.listFiles( + mainActivity.getCurrentMainFragment().getPath(), + mainActivity.isRootExplorer(), + showHiddenFiles, + mode -> null, + hybridFileParcelable -> { + if (showHiddenFiles || !hybridFileParcelable.isHidden()) + if (hybridFileParcelable + .getName(mainActivity) + .toLowerCase() + .contains(s.toLowerCase())) { + hybridFileParcelables.add(hybridFileParcelable); + + searchRecyclerViewAdapter.submitList(hybridFileParcelables); + + searchRecyclerViewAdapter.notifyItemInserted(hybridFileParcelables.size() + 1); + } + return null; }); + } - initSearchViewColor(mainActivity); + private void saveRecentPreference(String s) { + + String preferenceString = + PreferenceManager.getDefaultSharedPreferences(mainActivity) + .getString(PreferencesConstants.PREFERENCE_RECENT_SEARCH_ITEMS, null); + + ArrayList recentSearches = + preferenceString != null + ? new Gson().fromJson(preferenceString, new TypeToken>() {}.getType()) + : new ArrayList<>(); + + if (s.isEmpty() || recentSearches.contains(s)) return; + + recentSearches.add(s); + + if (recentSearches.size() > 5) recentSearches.remove(0); + + PreferenceManager.getDefaultSharedPreferences(mainActivity) + .edit() + .putString( + PreferencesConstants.PREFERENCE_RECENT_SEARCH_ITEMS, new Gson().toJson(recentSearches)) + .apply(); + + initRecentSearches(mainActivity); } private void initRecentSearches(Context context) { @@ -166,8 +222,10 @@ private void initRecentSearches(Context context) { chip.setOnClickListener( v -> { - searchListener.onSearch(((Chip) v).getText().toString()); - appbar.getSearchView().hideSearchView(); + String s = ((Chip) v).getText().toString(); + + searchViewEditText.setText(s); + search(s); }); } } @@ -251,6 +309,8 @@ public void hideSearchView() { animator = ObjectAnimator.ofFloat(searchViewLayout, "alpha", 1f, 0f); } + clearRecyclerView(); + // removing background fade view mainActivity.hideSmokeScreen(); animator.setInterpolator(new AccelerateDecelerateInterpolator()); @@ -316,6 +376,14 @@ private void initSearchViewColor(MainActivity a) { } } + @SuppressLint("NotifyDataSetChanged") + private void clearRecyclerView() { + searchRecyclerViewAdapter.submitList(new ArrayList<>()); + searchRecyclerViewAdapter.notifyDataSetChanged(); + + searchResultsHintTV.setVisibility(View.GONE); + } + public interface SearchListener { void onSearch(String queue); } diff --git a/app/src/main/res/layout-v21/layout_search.xml b/app/src/main/res/layout-v21/layout_search.xml index 3757da7b63..88222e41bf 100644 --- a/app/src/main/res/layout-v21/layout_search.xml +++ b/app/src/main/res/layout-v21/layout_search.xml @@ -1,97 +1,134 @@ - - + tools:ignore="ContentDescription"> - + - + - + - + - + + + + + + + android:layout_marginBottom="4dp" + android:text="Results" + android:textColor="@color/accent_material_light" + android:textSize="12sp" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/searchRecyclerView" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/searchRecentItemsScrollView" /> - + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout-w720dp/layout_search.xml b/app/src/main/res/layout-w720dp/layout_search.xml index 0da790bdf4..791fad9dc0 100644 --- a/app/src/main/res/layout-w720dp/layout_search.xml +++ b/app/src/main/res/layout-w720dp/layout_search.xml @@ -1,94 +1,112 @@ - - - - + tools:ignore="ContentDescription"> - + - + - + - + android:layout_marginBottom="4dp" + android:text="Recent" + android:textColor="@color/accent_material_light" + android:textSize="12sp" + app:layout_constraintBottom_toTopOf="@id/searchRecentItemsChipGroup" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/search_edit_text" /> + + - + + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_search.xml b/app/src/main/res/layout/layout_search.xml index 04bc137d5e..a292e6af6b 100644 --- a/app/src/main/res/layout/layout_search.xml +++ b/app/src/main/res/layout/layout_search.xml @@ -1,94 +1,113 @@ - - - - + tools:ignore="ContentDescription"> - + - + - + - + android:layout_marginBottom="4dp" + android:text="Recent" + android:textColor="@color/accent_material_light" + android:textSize="12sp" + app:layout_constraintBottom_toTopOf="@id/searchRecentItemsChipGroup" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/search_edit_text" /> + + - + + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_row_item.xml b/app/src/main/res/layout/search_row_item.xml new file mode 100644 index 0000000000..9bfca31161 --- /dev/null +++ b/app/src/main/res/layout/search_row_item.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + \ No newline at end of file From 5956b628111b5916cce8608bec6702ffbf1bf93e Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Mon, 10 Apr 2023 01:38:38 +0530 Subject: [PATCH 068/384] 3733: Refactor code --- .../filesystem/files/FileUtils.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java index 7689724997..ddead630b6 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java @@ -395,18 +395,17 @@ public static void shareFiles( ArrayList uris = new ArrayList<>(); boolean isGenericFileType = false; - for (File f : files) - uris.add(FileProvider.getUriForFile(activity, activity.getPackageName(), f)); - - String mime = MimeTypes.getMimeType(files.get(0).getPath(), files.get(0).isDirectory()); + String mime = files.size() > 1 ? MimeTypes.getMimeType(files.get(0).getPath(), + files.get(0).isDirectory()) : null; - if (files.size() > 1) - for (File file : files) - if (mime == null - || !mime.equals(MimeTypes.getMimeType(file.getPath(), file.isDirectory()))) { - isGenericFileType = true; - break; - } + for (File f : files) { + uris.add(FileProvider.getUriForFile(activity, activity.getPackageName(), f)); + if (mime == null + || !mime.equals(MimeTypes.getMimeType(f.getPath(), f.isDirectory()))) { + isGenericFileType = true; + break; + } + } if (isGenericFileType || mime == null) mime = MimeTypes.ALL_MIME_TYPES; From b5199fff882416f86db57cb65b3937b18b1c9655 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Mon, 10 Apr 2023 01:43:50 +0530 Subject: [PATCH 069/384] 3733: Refactor code --- .../amaze/filemanager/filesystem/files/FileUtils.java | 11 ++++++----- gradle.properties | 4 +++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java index ddead630b6..d55e7f09b3 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java @@ -395,15 +395,16 @@ public static void shareFiles( ArrayList uris = new ArrayList<>(); boolean isGenericFileType = false; - String mime = files.size() > 1 ? MimeTypes.getMimeType(files.get(0).getPath(), - files.get(0).isDirectory()) : null; + String mime = + files.size() > 1 + ? MimeTypes.getMimeType(files.get(0).getPath(), files.get(0).isDirectory()) + : null; for (File f : files) { uris.add(FileProvider.getUriForFile(activity, activity.getPackageName(), f)); - if (mime == null - || !mime.equals(MimeTypes.getMimeType(f.getPath(), f.isDirectory()))) { + if (!isGenericFileType + && (mime == null || !mime.equals(MimeTypes.getMimeType(f.getPath(), f.isDirectory())))) { isGenericFileType = true; - break; } } diff --git a/gradle.properties b/gradle.properties index 31554fde00..43a38b9251 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,6 @@ org.gradle.jvmargs=-Xmx4608M -XX:+UseParallelGC org.gradle.parallel=true # https://github.com/usefulness/easylauncher-gradle-plugin/issues/408 -android.disableResourceValidation=true \ No newline at end of file +android.disableResourceValidation=true +# for macs, omit for other operating systems +#org.gradle.java.home=/Applications/Android Studio.app/Contents/jre/Contents/Home \ No newline at end of file From 628175538033a5e0f45a2b21e2222cac38d34713 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Wed, 12 Apr 2023 22:30:50 +0530 Subject: [PATCH 070/384] fixes #3780 --- .../filemanager/adapters/AppsRecyclerAdapter.kt | 4 ++++ .../amaze/filemanager/adapters/holders/AppHolder.kt | 4 ++++ app/src/main/res/layout/rowlayout.xml | 12 +++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt index 7f4022e692..a2188bef0e 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt @@ -184,6 +184,10 @@ class AppsRecyclerAdapter( ) } holder.txtTitle.text = rowItem.label + + holder.packageName.text = rowItem.packageName + holder.packageName.isSelected = true // for marquee + val enableMarqueeFilename = (fragment.requireActivity() as MainActivity) .getBoolean(PreferencesConstants.PREFERENCE_ENABLE_MARQUEE_FILENAME) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt index 97987f2000..cfb5b5cca4 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt @@ -48,8 +48,12 @@ class AppHolder(view: View) : RecyclerView.ViewHolder(view) { @JvmField val summary: RelativeLayout = view.findViewById(R.id.summary) + @JvmField + val packageName: TextView = view.findViewById(R.id.appManagerPackageName) + init { apkIcon.visibility = View.VISIBLE + packageName.visibility = View.VISIBLE view.findViewById(R.id.picture_icon).visibility = View.GONE view.findViewById(R.id.generic_icon).visibility = View.GONE } diff --git a/app/src/main/res/layout/rowlayout.xml b/app/src/main/res/layout/rowlayout.xml index f168515e8b..79d05255c5 100644 --- a/app/src/main/res/layout/rowlayout.xml +++ b/app/src/main/res/layout/rowlayout.xml @@ -133,11 +133,21 @@ android:clickable="false" android:textSize="17sp" /> + + Date: Sun, 23 Apr 2023 11:51:06 +0800 Subject: [PATCH 071/384] Use HybridFile.getParent() instead of path string check for SSH server cases For navigating back to local device from remote SSH when pressing back. Fixes #3738 --- .../amaze/filemanager/ui/fragments/MainFragment.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 a96701c16f..5abc495d5e 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 @@ -26,7 +26,6 @@ import static android.os.Build.VERSION_CODES.Q; import static com.amaze.filemanager.filesystem.FileProperties.ANDROID_DATA_DIRS; import static com.amaze.filemanager.filesystem.FileProperties.ANDROID_DEVICE_DATA_DIRS; -import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_DIVIDERS; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_GOBACK_BUTTON; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES; @@ -1040,7 +1039,8 @@ public void goBack() { .getSmbPath() .equals(mainFragmentViewModel.getCurrentPath())) { StringBuilder path = new StringBuilder(currentFile.getSmbFile().getParent()); - if (mainFragmentViewModel.getCurrentPath().indexOf('?') > 0) + if (mainFragmentViewModel.getCurrentPath() != null + && mainFragmentViewModel.getCurrentPath().indexOf('?') > 0) path.append( mainFragmentViewModel .getCurrentPath() @@ -1052,11 +1052,7 @@ public void goBack() { false); } else loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); } else if (OpenMode.SFTP.equals(mainFragmentViewModel.getOpenMode())) { - if (mainFragmentViewModel.getCurrentPath() != null - && !mainFragmentViewModel - .getCurrentPath() - .substring(SSH_URI_PREFIX.length()) - .contains("/")) { + if (currentFile.getParent(requireContext()) == null) { loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); } else if (OpenMode.DOCUMENT_FILE.equals(mainFragmentViewModel.getOpenMode())) { loadlist(currentFile.getParent(getContext()), true, currentFile.getMode(), false); From 777ea95810a090d7345c4a6f425537c97d8110af Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sun, 30 Apr 2023 10:16:08 +0530 Subject: [PATCH 072/384] fixes #3790 --- app/src/main/java/com/amaze/filemanager/utils/Utils.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 4f0e35488d..1fca4ea923 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/Utils.java +++ b/app/src/main/java/com/amaze/filemanager/utils/Utils.java @@ -444,14 +444,16 @@ public static void addShortcut( shortcutIntent.setAction(Intent.ACTION_MAIN); shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + String fileName = new File(path.desc).getName(); + // 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()) + .setLongLabel(fileName) + .setShortLabel(fileName) .build(); ShortcutManagerCompat.requestPinShortcut(context, info, null); From d853c66ed72b2716b44cb2281e9a432542887888 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 2 May 2023 22:08:23 +0530 Subject: [PATCH 073/384] fixes #3790 - use `path.title` --- app/src/main/java/com/amaze/filemanager/utils/Utils.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 1fca4ea923..009e25d58b 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/Utils.java +++ b/app/src/main/java/com/amaze/filemanager/utils/Utils.java @@ -444,16 +444,14 @@ public static void addShortcut( shortcutIntent.setAction(Intent.ACTION_MAIN); shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - String fileName = new File(path.desc).getName(); - // 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(fileName) - .setShortLabel(fileName) + .setLongLabel(path.title) + .setShortLabel(path.title) .build(); ShortcutManagerCompat.requestPinShortcut(context, info, null); From 860892834b6141e66367c4f995db7750b8e3635a Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Wed, 3 May 2023 01:24:19 +0530 Subject: [PATCH 074/384] Add amaze utilities fdroid link --- README.md | 2 +- app/src/main/AndroidManifest.xml | 13 ++++++------- .../filemanager/ui/activities/AboutActivity.java | 2 ++ .../ui/activities/UtilitiesAliasActivity.kt | 7 ++++++- app/src/main/res/values/strings.xml | 7 +++---- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 92a1629ca1..3d9682746b 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Support PayPal Liberapay Or buy the [Cloud Plugin](https://play.google.com/store/apps/details?id=com.filemanager.amazecloud) supports Google Drive™, Dropbox, OneDrive and Box accounts. -Try our app - [Amaze File Utilities](https://play.google.com/store/apps/details?id=com.amaze.fileutilities) +Try our app - [Amaze File Utilities](https://play.google.com/store/apps/details?id=com.amaze.fileutilities) ([Fdroid](https://f-droid.org/en/packages/com.amaze.fileutilities/)) 1. List videos / images / music documents in your device in a interactive UI where you're able to group / sort and quickly jump to any headers. 2. Open videos / images / music / documents (pdf / docx / epub) with inbuilt player. 3. Share / delete / cast on your tv diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 42e6e9ae77..3eb63f4c86 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -240,14 +240,13 @@ tools:ignore="AppLinkUrlError"> - - - - - + + + + + + diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java index f41d14dcf1..5bd40d7128 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java @@ -86,6 +86,8 @@ public class AboutActivity extends ThemedActivity implements View.OnClickListene private static final String URL_REPO_RATE = "market://details?id=com.amaze.filemanager"; public static final String PACKAGE_AMAZE_UTILS = "com.amaze.fileutilities"; public static final String URL_AMAZE_UTILS = "market://details?id=" + PACKAGE_AMAZE_UTILS; + public static final String URL_AMAZE_UTILS_FDROID = + "https://f-droid.org/en/packages/" + PACKAGE_AMAZE_UTILS + "/"; @Override public void onCreate(@Nullable Bundle savedInstanceState) { diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/UtilitiesAliasActivity.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/UtilitiesAliasActivity.kt index 883ac40e11..11bbbbc0a2 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/UtilitiesAliasActivity.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/UtilitiesAliasActivity.kt @@ -23,6 +23,7 @@ package com.amaze.filemanager.ui.activities import android.content.ActivityNotFoundException import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.amaze.filemanager.BuildConfig import com.amaze.filemanager.R import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.databinding.UtilitiesAliasLayoutBinding @@ -44,7 +45,11 @@ class UtilitiesAliasActivity : AppCompatActivity() { setContentView(_binding.root) _binding.downloadButton.setOnClickListener { Utils.openURL( - AboutActivity.URL_AMAZE_UTILS, + if (BuildConfig.IS_VERSION_FDROID) { + AboutActivity.URL_AMAZE_UTILS_FDROID + } else { + AboutActivity.URL_AMAZE_UTILS + }, this ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ef5b0be6f..012b7811d1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -773,16 +773,15 @@ You only need to do this once, until the next time you select a new location for Cannot process the requested archive Analyse Storage Amaze File Utilities - You\'re being redirected to Amaze File Utilities. It\'s our offering that has inbuilt audio/video/image/document viewers, allows you to analyse storage files for blurry/dark images, duplicate files, fast transfer using Wifi P2P and much more.\nPlease support us by trying it out. + You\'re being redirected to Amaze File Utilities. It\'s our open source app that has inbuilt audio/video/image/document viewers, allows you to analyse junk files to free up storage, fast transfer using Wifi P2P and much more.\nPlease support us by trying it out. Download Wi-Fi P2P Thank you for installing Amaze File Utilities. You\'re being redirected to the app. Amaze Image Viewer Amaze Video Player Amaze Music Player - Amaze Pdf Viewer - Amaze Epub Viewer - Amaze Doc Viewer + Amaze Document Viewer + Amaze Document Viewer Share logs Share captured logs via email / telegram Open with Amaze From da13eb5a6253353a6b1cf3b00c7df6ba242498ad Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Wed, 3 May 2023 12:42:08 +0530 Subject: [PATCH 075/384] [WIP] simple search -- update UI --- .../adapters/SearchRecyclerViewAdapter.kt | 27 ++++++++------- .../ui/views/appbar/SearchView.java | 1 + app/src/main/res/layout/search_row_item.xml | 34 ++----------------- 3 files changed, 17 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt index b9e7eefae5..42b4f6a400 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt @@ -20,7 +20,7 @@ package com.amaze.filemanager.adapters -import android.text.format.Formatter +import android.graphics.Color import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -30,8 +30,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R import com.amaze.filemanager.filesystem.HybridFileParcelable -import com.amaze.filemanager.utils.Utils -import java.util.* +import java.util.Random class SearchRecyclerViewAdapter : ListAdapter( @@ -62,28 +61,30 @@ class SearchRecyclerViewAdapter : val item = getItem(position) holder.fileNameTV.text = item.name - - holder.dateTV.text = Utils.getDate(holder.itemView.context, item.date) - - if (!item.isDirectory) { - holder.sizeTV.text = Formatter.formatFileSize(holder.itemView.context, item.size) - } } inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val fileNameTV: TextView - val dateTV: TextView - val sizeTV: TextView init { view.setOnClickListener { } + view.findViewById(R.id.searchItemSampleColorView) + .setBackgroundColor(getRandomColor()) + fileNameTV = view.findViewById(R.id.searchItemFileNameTV) - dateTV = view.findViewById(R.id.searchItemDateTV) - sizeTV = view.findViewById(R.id.searchItemSizeTV) + } + + private fun getRandomColor(): Int { + val colorArray = arrayOf( + "#e57373", "#f06292", "#ba68c8", "#9575cd", "#7986cb", "#64b5f6", "#4fc3f7", + "#4dd0e1", "#4db6ac", "#81c784", "#aed581", "#dce775", "#fff176", "#ffd54f", + "#ffb74d", "#ff8a65", "#a1887f" + ) + return Color.parseColor(colorArray[Random().nextInt(colorArray.size - 1)]) } } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 4285ac71c6..4d10bd43c7 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -146,6 +146,7 @@ private void search(String s) { PreferenceManager.getDefaultSharedPreferences(mainActivity) .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false); + // TODO: takes too much resources & freezes main thread on huge folders ListFilesCommand.INSTANCE.listFiles( mainActivity.getCurrentMainFragment().getPath(), mainActivity.isRootExplorer(), diff --git a/app/src/main/res/layout/search_row_item.xml b/app/src/main/res/layout/search_row_item.xml index 9bfca31161..2069032c96 100644 --- a/app/src/main/res/layout/search_row_item.xml +++ b/app/src/main/res/layout/search_row_item.xml @@ -27,43 +27,13 @@ android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" - android:layout_marginBottom="2dp" + android:layout_marginBottom="8dp" android:textSize="16sp" - app:layout_constraintBottom_toTopOf="@id/searchItemSizeTV" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/searchItemSampleColorView" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0" /> - - - - \ No newline at end of file From b8f967c53f7b757ea7aeffc8b76901113a98b0e2 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Wed, 3 May 2023 22:55:26 +0530 Subject: [PATCH 076/384] simple search MVP --- .../adapters/SearchRecyclerViewAdapter.kt | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt index 42b4f6a400..c3f233ceb5 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt @@ -20,16 +20,20 @@ package com.amaze.filemanager.adapters -import android.graphics.Color +import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.ui.colors.ColorPreference import java.util.Random class SearchRecyclerViewAdapter : @@ -69,22 +73,39 @@ class SearchRecyclerViewAdapter : init { - view.setOnClickListener { - } + fileNameTV = view.findViewById(R.id.searchItemFileNameTV) view.findViewById(R.id.searchItemSampleColorView) - .setBackgroundColor(getRandomColor()) + .setBackgroundColor(getRandomColor(view.context)) - fileNameTV = view.findViewById(R.id.searchItemFileNameTV) + view.setOnClickListener { + + val item = getItem(adapterPosition) + + if (!item.isDirectory) { + item.openFile( + AppConfig.getInstance().mainActivityContext as MainActivity?, + false + ) + } else { + (AppConfig.getInstance().mainActivityContext as MainActivity?) + ?.goToMain(item.path) + } + + (AppConfig.getInstance().mainActivityContext as MainActivity?) + ?.appbar?.searchView?.hideSearchView() + } } - private fun getRandomColor(): Int { - val colorArray = arrayOf( - "#e57373", "#f06292", "#ba68c8", "#9575cd", "#7986cb", "#64b5f6", "#4fc3f7", - "#4dd0e1", "#4db6ac", "#81c784", "#aed581", "#dce775", "#fff176", "#ffd54f", - "#ffb74d", "#ff8a65", "#a1887f" + private fun getRandomColor(context: Context): Int { + return ContextCompat.getColor( + context, + ColorPreference.availableColors[ + Random().nextInt( + ColorPreference.availableColors.size - 1 + ) + ] ) - return Color.parseColor(colorArray[Random().nextInt(colorArray.size - 1)]) } } } From 2ef5e197ab3a0ce6f1ad390d846225a602071cdf Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Wed, 3 May 2023 23:49:17 +0530 Subject: [PATCH 077/384] Hide get cloud connections button in fdroid flavour --- .../filemanager/ui/fragments/CloudSheetFragment.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java index 626eb534bd..648abc8b3c 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java @@ -20,6 +20,7 @@ package com.amaze.filemanager.ui.fragments; +import com.amaze.filemanager.BuildConfig; import com.amaze.filemanager.R; import com.amaze.filemanager.database.CloudContract; import com.amaze.filemanager.databinding.FragmentSheetCloudBinding; @@ -121,6 +122,14 @@ public void setupDialog(Dialog dialog, int style) { mGetCloudLayout.setVisibility(View.GONE); } + if (BuildConfig.IS_VERSION_FDROID) { + mBoxLayout.setVisibility(View.GONE); + mDropboxLayout.setVisibility(View.GONE); + mGoogleDriveLayout.setVisibility(View.GONE); + mOnedriveLayout.setVisibility(View.GONE); + mGetCloudLayout.setVisibility(View.GONE); + } + mSmbLayout.setOnClickListener(this); mScpLayout.setOnClickListener(this); mBoxLayout.setOnClickListener(this); From fd2e482722014cdd5eeca18774fb462189e104ad Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Tue, 9 May 2023 05:14:24 +0530 Subject: [PATCH 078/384] 3808 Update gradle-build-action in workflow --- .github/workflows/android-build.yml | 4 ++-- .github/workflows/android-debug-artifact-ondemand.yml | 2 +- .github/workflows/android-debug-artifact-release.yml | 2 +- .github/workflows/android-feature.yml | 6 +++--- .github/workflows/android-main.yml | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 6e35887d7f..23c42fb88e 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -22,7 +22,7 @@ jobs: distribution: "temurin" java-version: 11 - name: Check formatting using spotless - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: spotlessCheck --stacktrace @@ -37,6 +37,6 @@ jobs: distribution: "temurin" java-version: 11 - name: Build with Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: assembledebug --stacktrace \ No newline at end of file diff --git a/.github/workflows/android-debug-artifact-ondemand.yml b/.github/workflows/android-debug-artifact-ondemand.yml index 03a7419914..92601c5eaa 100644 --- a/.github/workflows/android-debug-artifact-ondemand.yml +++ b/.github/workflows/android-debug-artifact-ondemand.yml @@ -41,7 +41,7 @@ jobs: distribution: "temurin" java-version: 11 - name: Build with Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: assembleDebug --stacktrace - name: Upload fdroid artifact diff --git a/.github/workflows/android-debug-artifact-release.yml b/.github/workflows/android-debug-artifact-release.yml index 7c28fe286a..14604bfd2e 100644 --- a/.github/workflows/android-debug-artifact-release.yml +++ b/.github/workflows/android-debug-artifact-release.yml @@ -15,7 +15,7 @@ jobs: distribution: "temurin" java-version: 11 - name: Build with Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: assembleDebug - name: Upload fdroid artifact diff --git a/.github/workflows/android-feature.yml b/.github/workflows/android-feature.yml index c661c27df0..3eeb27438f 100644 --- a/.github/workflows/android-feature.yml +++ b/.github/workflows/android-feature.yml @@ -23,7 +23,7 @@ jobs: distribution: "temurin" java-version: 11 - name: Check formatting using spotless - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: spotlessCheck --stacktrace @@ -38,10 +38,10 @@ jobs: distribution: "temurin" java-version: 11 - name: Build with Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: assembledebug --stacktrace - name: Run test cases - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: jacocoTestPlayDebugUnitTestReport --stacktrace --info \ No newline at end of file diff --git a/.github/workflows/android-main.yml b/.github/workflows/android-main.yml index 1c01d0b95c..5d15d1c254 100644 --- a/.github/workflows/android-main.yml +++ b/.github/workflows/android-main.yml @@ -23,7 +23,7 @@ jobs: distribution: "temurin" java-version: 11 - name: Check formatting using spotless - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: spotlessCheck @@ -40,11 +40,11 @@ jobs: distribution: "temurin" java-version: 11 - name: Build with Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: assembledebug - name: Run test cases - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: arguments: jacocoTestPlayDebugUnitTestReport - name: Publish test cases @@ -76,7 +76,7 @@ jobs: with: java-version: 15 - name: Gradle cache - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 - name: AVD cache uses: actions/cache@v2 id: avd-cache From be5d436d93f4c6096eb0b5c811dc2479e4cec76e Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 26 May 2023 14:08:19 +0530 Subject: [PATCH 079/384] indexed search MVP --- .../ui/views/appbar/SearchView.java | 127 +++++++++++++++++- app/src/main/res/layout-v21/layout_search.xml | 18 ++- .../main/res/layout-w720dp/layout_search.xml | 18 ++- app/src/main/res/layout/layout_search.xml | 18 ++- app/src/main/res/values/strings.xml | 3 + 5 files changed, 175 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 4d10bd43c7..429b33e728 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -24,11 +24,14 @@ import static android.os.Build.VERSION.SDK_INT; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES; +import java.io.File; import java.util.ArrayList; +import java.util.List; import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.SearchRecyclerViewAdapter; import com.amaze.filemanager.filesystem.HybridFileParcelable; +import com.amaze.filemanager.filesystem.RootHelper; import com.amaze.filemanager.filesystem.root.ListFilesCommand; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; @@ -43,7 +46,11 @@ import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; +import android.database.Cursor; import android.graphics.PorterDuff; +import android.provider.MediaStore; +import android.text.Editable; +import android.text.TextWatcher; import android.view.ContextThemeWrapper; import android.view.View; import android.view.ViewAnimationUtils; @@ -77,6 +84,7 @@ public class SearchView { private final TextView recentHintTV; private final TextView searchResultsHintTV; + private final TextView deepSearchTV; private final ChipGroup recentChipGroup; private final RecyclerView recyclerView; @@ -84,8 +92,15 @@ public class SearchView { private final SearchListener searchListener; private final SearchRecyclerViewAdapter searchRecyclerViewAdapter; + // 0 -> Basic + // 1 -> Indexed + // 2 -> Recursive + private int searchMode; + private boolean enabled = false; + @SuppressWarnings("ConstantConditions") + @SuppressLint("NotifyDataSetChanged") public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener searchListener) { this.mainActivity = mainActivity; @@ -99,8 +114,16 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener recentChipGroup = mainActivity.findViewById(R.id.searchRecentItemsChipGroup); recentHintTV = mainActivity.findViewById(R.id.searchRecentHintTV); searchResultsHintTV = mainActivity.findViewById(R.id.searchResultsHintTV); + deepSearchTV = mainActivity.findViewById(R.id.searchDeepSearchTV); recyclerView = mainActivity.findViewById(R.id.searchRecyclerView); + searchMode = 0; + deepSearchTV.setText( + String.format( + "%s%s", + mainActivity.getString(R.string.not_finding_what_you_re_looking_for), + mainActivity.getString(R.string.try_indexed_search))); + initRecentSearches(mainActivity); searchRecyclerViewAdapter = new SearchRecyclerViewAdapter(); @@ -116,29 +139,86 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener backImageView.setOnClickListener(v -> appbar.getSearchView().hideSearchView()); + searchViewEditText.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + if (count > 0) searchViewEditText.setError(null); + + if (count >= 3) onSearch(false); + } + + @Override + public void afterTextChanged(Editable s) {} + }); + searchViewEditText.setOnEditorActionListener( (v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_SEARCH) { + if (actionId == EditorInfo.IME_ACTION_SEARCH) return onSearch(true); + + return false; + }); - String s = searchViewEditText.getText().toString().trim(); + deepSearchTV.setOnClickListener( + v -> { + if (searchMode == 1) { - search(s); + List hybridFileParcelables = + indexedSearch(mainActivity, searchViewEditText.getText().toString().trim()); + + searchRecyclerViewAdapter.submitList(hybridFileParcelables); + searchRecyclerViewAdapter.notifyDataSetChanged(); + + searchMode = 2; + deepSearchTV.setText( + String.format( + "%s%s", + mainActivity.getString(R.string.not_finding_what_you_re_looking_for), + mainActivity.getString(R.string.try_recursive_search))); - saveRecentPreference(s); + } else if (searchMode == 2) { - return true; + deepSearchTV.setVisibility(View.GONE); } - return false; }); initSearchViewColor(mainActivity); } + @SuppressWarnings("ConstantConditions") + private boolean onSearch(boolean shouldSave) { + + String s = searchViewEditText.getText().toString().trim(); + + if (s.isEmpty()) { + searchViewEditText.setError(mainActivity.getString(R.string.field_empty)); + searchViewEditText.requestFocus(); + return false; + } + + search(s); + + if (shouldSave) saveRecentPreference(s); + + return true; + } + private void search(String s) { clearRecyclerView(); searchResultsHintTV.setVisibility(View.VISIBLE); + deepSearchTV.setVisibility(View.VISIBLE); + searchMode = 1; + deepSearchTV.setText( + String.format( + "%s%s", + mainActivity.getString(R.string.not_finding_what_you_re_looking_for), + mainActivity.getString(R.string.try_indexed_search))); ArrayList hybridFileParcelables = new ArrayList<>(); @@ -231,6 +311,41 @@ private void initRecentSearches(Context context) { } } + private List indexedSearch(MainActivity mainActivity, String query) { + + ArrayList list = new ArrayList<>(); + final String[] projection = {MediaStore.Files.FileColumns.DATA}; + + Cursor cursor = + mainActivity + .getContentResolver() + .query(MediaStore.Files.getContentUri("external"), projection, null, null, null); + + if (cursor == null) return list; + else if (cursor.getCount() > 0 && cursor.moveToFirst()) { + do { + String path = + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)); + + if (path != null + && path.contains(mainActivity.getCurrentMainFragment().getPath()) + && path.toLowerCase().contains(query.toLowerCase())) { + + boolean showHiddenFiles = + PreferenceManager.getDefaultSharedPreferences(mainActivity) + .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false); + + HybridFileParcelable hybridFileParcelable = + RootHelper.generateBaseFile(new File(path), showHiddenFiles); + + if (hybridFileParcelable != null) list.add(hybridFileParcelable); + } + } while (cursor.moveToNext()); + } + cursor.close(); + return list; + } + /** show search view with a circular reveal animation */ public void revealSearchView() { final int START_RADIUS = 16; diff --git a/app/src/main/res/layout-v21/layout_search.xml b/app/src/main/res/layout-v21/layout_search.xml index 88222e41bf..eda8994a46 100644 --- a/app/src/main/res/layout-v21/layout_search.xml +++ b/app/src/main/res/layout-v21/layout_search.xml @@ -125,10 +125,26 @@ android:layout_height="wrap_content" android:nestedScrollingEnabled="false" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/searchDeepSearchTV" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/searchResultsHintTV" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout-w720dp/layout_search.xml b/app/src/main/res/layout-w720dp/layout_search.xml index 791fad9dc0..03e740801b 100644 --- a/app/src/main/res/layout-w720dp/layout_search.xml +++ b/app/src/main/res/layout-w720dp/layout_search.xml @@ -103,10 +103,26 @@ android:layout_width="@dimen/zero_dp" android:layout_height="wrap_content" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/searchDeepSearchTV" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/searchRecentItemsScrollView" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_search.xml b/app/src/main/res/layout/layout_search.xml index a292e6af6b..c81bcdc7f3 100644 --- a/app/src/main/res/layout/layout_search.xml +++ b/app/src/main/res/layout/layout_search.xml @@ -104,10 +104,26 @@ android:layout_height="wrap_content" android:nestedScrollingEnabled="false" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/searchDeepSearchTV" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/searchRecentItemsScrollView" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ef5b0be6f..24254559dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -809,5 +809,8 @@ You only need to do this once, until the next time you select a new location for No app found to handle this intent. Do you have DocumentsUI installed? Cloud Connection credentials cleared Unfortunately, we were unable to migrate your cloud connection credentials to the new database schema, and we had to remove them from the app. Please create the cloud connection again. + Not finding what you\'re looking for? + Try Recursive Search! + Try Indexed Search! From eb2a7ac456138546fd1f39b11fa85cef26f52322 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 26 May 2023 15:06:12 +0530 Subject: [PATCH 080/384] improve UX --- .../ui/views/appbar/SearchView.java | 16 +++++++--- .../com/amaze/filemanager/utils/Utils.java | 8 +++++ app/src/main/res/layout-v21/layout_search.xml | 10 +++--- .../main/res/layout-w720dp/layout_search.xml | 30 ++++++++++++++---- app/src/main/res/layout/layout_search.xml | 31 ++++++++++++++----- app/src/main/res/values/strings.xml | 2 ++ 6 files changed, 75 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 429b33e728..48751fe34c 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -120,7 +120,7 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener searchMode = 0; deepSearchTV.setText( String.format( - "%s%s", + "%s %s", mainActivity.getString(R.string.not_finding_what_you_re_looking_for), mainActivity.getString(R.string.try_indexed_search))); @@ -158,7 +158,12 @@ public void afterTextChanged(Editable s) {} searchViewEditText.setOnEditorActionListener( (v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_SEARCH) return onSearch(true); + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + + Utils.hideKeyboard(mainActivity); + + return onSearch(true); + } return false; }); @@ -176,7 +181,7 @@ public void afterTextChanged(Editable s) {} searchMode = 2; deepSearchTV.setText( String.format( - "%s%s", + "%s %s", mainActivity.getString(R.string.not_finding_what_you_re_looking_for), mainActivity.getString(R.string.try_recursive_search))); @@ -216,7 +221,7 @@ private void search(String s) { searchMode = 1; deepSearchTV.setText( String.format( - "%s%s", + "%s %s", mainActivity.getString(R.string.not_finding_what_you_re_looking_for), mainActivity.getString(R.string.try_indexed_search))); @@ -306,6 +311,9 @@ private void initRecentSearches(Context context) { String s = ((Chip) v).getText().toString(); searchViewEditText.setText(s); + + Utils.hideKeyboard(mainActivity); + search(s); }); } 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 009e25d58b..8c7f0d44be 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/Utils.java +++ b/app/src/main/java/com/amaze/filemanager/utils/Utils.java @@ -50,6 +50,7 @@ import android.text.format.DateUtils; import android.util.DisplayMetrics; import android.view.View; +import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.CheckBox; import android.widget.TextView; @@ -456,4 +457,11 @@ public static void addShortcut( ShortcutManagerCompat.requestPinShortcut(context, info, null); } + + public static void hideKeyboard(MainActivity mainActivity) { + View view = mainActivity.getCurrentFocus(); + if (view != null) + ((InputMethodManager) mainActivity.getSystemService(Context.INPUT_METHOD_SERVICE)) + .hideSoftInputFromWindow(view.getWindowToken(), 0); + } } diff --git a/app/src/main/res/layout-v21/layout_search.xml b/app/src/main/res/layout-v21/layout_search.xml index eda8994a46..ed8aaa8dcb 100644 --- a/app/src/main/res/layout-v21/layout_search.xml +++ b/app/src/main/res/layout-v21/layout_search.xml @@ -33,7 +33,7 @@ + + + app:layout_constraintTop_toBottomOf="@id/searchResultsHintTV" /> + + + app:layout_constraintTop_toBottomOf="@id/searchResultsHintTV" /> Not finding what you\'re looking for? Try Recursive Search! Try Indexed Search! + Recent + Results From f7dccf8b7c0280e006a06767d2aec31a14f3ad6b Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 26 May 2023 15:12:34 +0530 Subject: [PATCH 081/384] recursive search MVP --- .../com/amaze/filemanager/ui/views/appbar/SearchView.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 48751fe34c..4dbc7a7c93 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -170,10 +170,11 @@ public void afterTextChanged(Editable s) {} deepSearchTV.setOnClickListener( v -> { + String s = searchViewEditText.getText().toString().trim(); + if (searchMode == 1) { - List hybridFileParcelables = - indexedSearch(mainActivity, searchViewEditText.getText().toString().trim()); + List hybridFileParcelables = indexedSearch(mainActivity, s); searchRecyclerViewAdapter.submitList(hybridFileParcelables); searchRecyclerViewAdapter.notifyDataSetChanged(); @@ -187,6 +188,9 @@ public void afterTextChanged(Editable s) {} } else if (searchMode == 2) { + searchListener.onSearch(s); + appbar.getSearchView().hideSearchView(); + deepSearchTV.setVisibility(View.GONE); } }); From ce8bfbd13aee7bc288aec3b3eae2a8cf4a4a0867 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 26 May 2023 15:33:16 +0530 Subject: [PATCH 082/384] UI --- .../adapters/SearchRecyclerViewAdapter.kt | 35 ++++++++++++------- .../ui/views/appbar/SearchView.java | 1 + 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt index c3f233ceb5..ba44316631 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt @@ -65,18 +65,27 @@ class SearchRecyclerViewAdapter : val item = getItem(position) holder.fileNameTV.text = item.name + holder.colorView.setBackgroundColor(getRandomColor(holder.colorView.context)) + +// val colorPreference = +// (AppConfig.getInstance().mainActivityContext as MainActivity).currentColorPreference +// +// if (item != null && item.isDirectory) { // always null for some reason! +// holder.colorView.setBackgroundColor(colorPreference.iconSkin) +// } else { +// holder.colorView.setBackgroundColor(colorPreference.accent) +// } } inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val fileNameTV: TextView + val colorView: View init { fileNameTV = view.findViewById(R.id.searchItemFileNameTV) - - view.findViewById(R.id.searchItemSampleColorView) - .setBackgroundColor(getRandomColor(view.context)) + colorView = view.findViewById(R.id.searchItemSampleColorView) view.setOnClickListener { @@ -96,16 +105,16 @@ class SearchRecyclerViewAdapter : ?.appbar?.searchView?.hideSearchView() } } + } - private fun getRandomColor(context: Context): Int { - return ContextCompat.getColor( - context, - ColorPreference.availableColors[ - Random().nextInt( - ColorPreference.availableColors.size - 1 - ) - ] - ) - } + private fun getRandomColor(context: Context): Int { + return ContextCompat.getColor( + context, + ColorPreference.availableColors[ + Random().nextInt( + ColorPreference.availableColors.size - 1 + ) + ] + ) } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 4dbc7a7c93..525f7a45ce 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -123,6 +123,7 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener "%s %s", mainActivity.getString(R.string.not_finding_what_you_re_looking_for), mainActivity.getString(R.string.try_indexed_search))); + deepSearchTV.setVisibility(View.GONE); initRecentSearches(mainActivity); From 8c097529b42b53c42c0560a0c606da0bf1e6e23a Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 26 May 2023 15:35:42 +0530 Subject: [PATCH 083/384] fix bug in indexed search --- .../java/com/amaze/filemanager/ui/views/appbar/SearchView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 525f7a45ce..3f71245056 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -342,7 +342,7 @@ else if (cursor.getCount() > 0 && cursor.moveToFirst()) { if (path != null && path.contains(mainActivity.getCurrentMainFragment().getPath()) - && path.toLowerCase().contains(query.toLowerCase())) { + && new File(path).getName().toLowerCase().contains(query.toLowerCase())) { boolean showHiddenFiles = PreferenceManager.getDefaultSharedPreferences(mainActivity) From 389dc54e2a60b96709845494453a5e687aafbc4c Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 26 May 2023 15:47:15 +0530 Subject: [PATCH 084/384] add file path to results --- .../adapters/SearchRecyclerViewAdapter.kt | 6 ++++- app/src/main/res/layout/search_row_item.xml | 22 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt index ba44316631..ad17c6caa9 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt @@ -65,12 +65,14 @@ class SearchRecyclerViewAdapter : val item = getItem(position) holder.fileNameTV.text = item.name + holder.filePathTV.text = item.path.substring(0, item.path.lastIndexOf("/")) + holder.colorView.setBackgroundColor(getRandomColor(holder.colorView.context)) // val colorPreference = // (AppConfig.getInstance().mainActivityContext as MainActivity).currentColorPreference // -// if (item != null && item.isDirectory) { // always null for some reason! +// if (item != null && item.isDirectory) { // always false for some reason! // holder.colorView.setBackgroundColor(colorPreference.iconSkin) // } else { // holder.colorView.setBackgroundColor(colorPreference.accent) @@ -80,11 +82,13 @@ class SearchRecyclerViewAdapter : inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val fileNameTV: TextView + val filePathTV: TextView val colorView: View init { fileNameTV = view.findViewById(R.id.searchItemFileNameTV) + filePathTV = view.findViewById(R.id.searchItemFilePathTV) colorView = view.findViewById(R.id.searchItemSampleColorView) view.setOnClickListener { diff --git a/app/src/main/res/layout/search_row_item.xml b/app/src/main/res/layout/search_row_item.xml index 2069032c96..0ff8072465 100644 --- a/app/src/main/res/layout/search_row_item.xml +++ b/app/src/main/res/layout/search_row_item.xml @@ -13,7 +13,7 @@ + + \ No newline at end of file From 0e2b2373102db57ecf126cf5c6027b37549478f4 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 26 May 2023 20:44:17 +0530 Subject: [PATCH 085/384] codacy: These nested if statements could be combined --- .../filemanager/ui/views/appbar/SearchView.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 3f71245056..bdbeffe16a 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -243,17 +243,15 @@ private void search(String s) { showHiddenFiles, mode -> null, hybridFileParcelable -> { - if (showHiddenFiles || !hybridFileParcelable.isHidden()) - if (hybridFileParcelable - .getName(mainActivity) - .toLowerCase() - .contains(s.toLowerCase())) { - hybridFileParcelables.add(hybridFileParcelable); + if (hybridFileParcelable.getName(mainActivity).toLowerCase().contains(s.toLowerCase()) + && (showHiddenFiles || !hybridFileParcelable.isHidden())) { - searchRecyclerViewAdapter.submitList(hybridFileParcelables); + hybridFileParcelables.add(hybridFileParcelable); - searchRecyclerViewAdapter.notifyItemInserted(hybridFileParcelables.size() + 1); - } + searchRecyclerViewAdapter.submitList(hybridFileParcelables); + + searchRecyclerViewAdapter.notifyItemInserted(hybridFileParcelables.size() + 1); + } return null; }); } From 9db1d72eaedac11dae314135b84a87825bddcd2f Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 26 May 2023 20:45:35 +0530 Subject: [PATCH 086/384] codacy: Avoid unused private fields such as 'searchListener'. --- .../java/com/amaze/filemanager/ui/views/appbar/SearchView.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index bdbeffe16a..f9a2b3e1a3 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -89,7 +89,6 @@ public class SearchView { private final ChipGroup recentChipGroup; private final RecyclerView recyclerView; - private final SearchListener searchListener; private final SearchRecyclerViewAdapter searchRecyclerViewAdapter; // 0 -> Basic @@ -104,7 +103,6 @@ public class SearchView { public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener searchListener) { this.mainActivity = mainActivity; - this.searchListener = searchListener; this.appbar = appbar; searchViewLayout = mainActivity.findViewById(R.id.search_view); From 27de7488e0a27b007736d1157daba01bbf08b4d4 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 27 May 2023 12:46:57 +0530 Subject: [PATCH 087/384] fixes #3778 Signed-off-by: VishnuSanal --- .../amaze/filemanager/ui/ItemPopupMenu.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java index c3798955fe..1785f87d5a 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java +++ b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java @@ -163,17 +163,24 @@ public boolean onMenuItemClick(MenuItem item) { case R.id.open_with: boolean useNewStack = sharedPrefs.getBoolean(PreferencesConstants.PREFERENCE_TEXTEDITOR_NEWSTACK, false); + if (OpenMode.DOCUMENT_FILE.equals(rowItem.getMode())) { + @Nullable Uri fullUri = rowItem.generateBaseFile().getFullUri(); + if (fullUri != null) { - FileUtils.openWith( - DocumentFile.fromSingleUri(context, fullUri), mainActivity, useNewStack); - } else { - FileUtils.openWith(new File(rowItem.desc), mainActivity, useNewStack); + + DocumentFile documentFile = DocumentFile.fromSingleUri(context, fullUri); + + if (documentFile != null) { + FileUtils.openWith(documentFile, mainActivity, useNewStack); + return true; + } } - } else { - FileUtils.openWith(new File(rowItem.desc), mainActivity, useNewStack); } + + FileUtils.openWith(new File(rowItem.desc), mainActivity, useNewStack); + return true; case R.id.encrypt: final Intent encryptIntent = new Intent(context, EncryptService.class); From f6c5bb4e565c90bfef1295f9a91d74040e6a01ea Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 27 May 2023 13:15:02 +0530 Subject: [PATCH 088/384] refractor `LoadFilesListTask#postListCustomPathProcess` Signed-off-by: VishnuSanal --- .../asynctasks/LoadFilesListTask.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 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 f22e03b570..57352a62b4 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 @@ -251,15 +251,15 @@ private List getCachedMediaList( private void postListCustomPathProcess( @NonNull List list, @NonNull MainFragment mainFragment) { - int t = SortHandler.getSortType(context.get(), path); - int sortby; - int asc; - if (t <= 3) { - sortby = t; - asc = 1; + + int sortType = SortHandler.getSortType(context.get(), path), sortBy, isAscending; + + if (sortType <= 3) { + sortBy = sortType; + isAscending = 1; } else { - asc = -1; - sortby = t - 4; + isAscending = -1; + sortBy = sortType - 4; } MainFragmentViewModel viewModel = mainFragment.getMainFragmentViewModel(); @@ -268,6 +268,7 @@ private void postListCustomPathProcess( LayoutElementParcelable layoutElementParcelable = list.get(i); if (layoutElementParcelable == null) { + //noinspection SuspiciousListRemoveInLoop list.remove(i); continue; } @@ -280,7 +281,7 @@ private void postListCustomPathProcess( } if (viewModel != null) { - Collections.sort(list, new FileListSorter(viewModel.getDsort(), sortby, asc)); + Collections.sort(list, new FileListSorter(viewModel.getDsort(), sortBy, isAscending)); } else { LOG.error("MainFragmentViewModel is null, this is a bug"); } From 1baed404ff843ed321db2abc2c280ccdeb2d70c5 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 27 May 2023 13:24:52 +0530 Subject: [PATCH 089/384] fixes #3746 Signed-off-by: VishnuSanal --- .../asynctasks/LoadFilesListTask.java | 16 +++++++++------- .../ui/fragments/data/MainFragmentViewModel.kt | 8 ++++++++ 2 files changed, 17 insertions(+), 7 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 57352a62b4..3a0bd8b158 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 @@ -264,6 +264,11 @@ private void postListCustomPathProcess( MainFragmentViewModel viewModel = mainFragment.getMainFragmentViewModel(); + if (viewModel == null) { + LOG.error("MainFragmentViewModel is null, this is a bug"); + return; + } + for (int i = 0; i < list.size(); i++) { LayoutElementParcelable layoutElementParcelable = list.get(i); @@ -274,17 +279,14 @@ private void postListCustomPathProcess( } if (layoutElementParcelable.isDirectory) { - viewModel.setFolderCount(mainFragment.getMainFragmentViewModel().getFolderCount() + 1); + viewModel.incrementFolderCount(); } else { - viewModel.setFileCount(mainFragment.getMainFragmentViewModel().getFileCount() + 1); + viewModel.incrementFileCount(); } } - if (viewModel != null) { - Collections.sort(list, new FileListSorter(viewModel.getDsort(), sortBy, isAscending)); - } else { - LOG.error("MainFragmentViewModel is null, this is a bug"); - } + Collections.sort(list, new FileListSorter(viewModel.getDsort(), sortBy, isAscending)); + } private @Nullable LayoutElementParcelable createListParcelables(HybridFileParcelable baseFile) { 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 33cad7f3a1..057d4b111d 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 @@ -243,4 +243,12 @@ class MainFragmentViewModel : ViewModel() { } return mutableLiveData } + + fun incrementFileCount() { + fileCount++ + } + + fun incrementFolderCount() { + folderCount++ + } } From 0b6e1dd593dc9fce3959217276f17806dceb7411 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 27 May 2023 16:19:55 +0530 Subject: [PATCH 090/384] chore: spotless Signed-off-by: VishnuSanal --- .../filemanager/asynchronous/asynctasks/LoadFilesListTask.java | 1 - 1 file changed, 1 deletion(-) 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 3a0bd8b158..0df0406f5f 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 @@ -286,7 +286,6 @@ private void postListCustomPathProcess( } Collections.sort(list, new FileListSorter(viewModel.getDsort(), sortBy, isAscending)); - } private @Nullable LayoutElementParcelable createListParcelables(HybridFileParcelable baseFile) { From 54f830f319de835ed859cc10133852bcf7008468 Mon Sep 17 00:00:00 2001 From: Oleksandr Narvatov Date: Sat, 27 May 2023 16:17:26 +0300 Subject: [PATCH 091/384] Fixes #3430 --- .../com/amaze/filemanager/ui/fragments/FtpServerFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt index 4e12699f06..31545e7b88 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt @@ -198,8 +198,8 @@ class FtpServerFragment : Fragment(R.layout.fragment_ftp) { val editText = dialog.inputEditText if (editText != null) { val name = editText.text.toString() - val portNumber = name.toInt() - if (portNumber < 1024) { + val portNumber = name.toIntOrNull() + if (portNumber == null || portNumber < 1024) { Toast.makeText( activity, R.string.ftp_port_change_error_invalid, From 4fce614261142d8a5ae406d3c0b56819666664b1 Mon Sep 17 00:00:00 2001 From: Oleksandr Narvatov Date: Sat, 27 May 2023 18:18:57 +0300 Subject: [PATCH 092/384] Fixes #3162 --- .../java/com/amaze/filemanager/ui/views/drawer/Drawer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java index c55fb95000..d1272f86ba 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java @@ -796,18 +796,19 @@ public void onDrawerClosed() { hFile.generateMode(mainActivity); if (hFile.isSimpleFile()) { FileUtils.openFile(new File(pendingPath), mainActivity, mainActivity.getPrefs()); - pendingPath = null; + resetPendingPath(); return; } MainFragment mainFragment = mainActivity.getCurrentMainFragment(); if (mainFragment != null) { mainFragment.loadlist(pendingPath, false, OpenMode.UNKNOWN, false); + resetPendingPath(); } else { mainActivity.goToMain(pendingPath); + resetPendingPath(); return; } - pendingPath = null; } mainActivity.supportInvalidateOptionsMenu(); } From cb6919491292fe5a5dbb1782904f2deb23d97a1a Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 27 May 2023 23:34:41 +0530 Subject: [PATCH 093/384] codacy: functions missing documentation. Signed-off-by: VishnuSanal --- .../filemanager/ui/fragments/data/MainFragmentViewModel.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 057d4b111d..1c236605c5 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 @@ -244,10 +244,16 @@ class MainFragmentViewModel : ViewModel() { return mutableLiveData } + /** + * increments `fileCount` + */ fun incrementFileCount() { fileCount++ } + /** + * increments `folderCount` + */ fun incrementFolderCount() { folderCount++ } From 4f536ac699764404b091f65d4c3604270e500e09 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sat, 27 May 2023 23:36:14 +0530 Subject: [PATCH 094/384] codacy: Use one line for each declaration, it enhances code readability :( Signed-off-by: VishnuSanal --- .../asynchronous/asynctasks/LoadFilesListTask.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0df0406f5f..4ae437aaa6 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 @@ -252,7 +252,9 @@ private List getCachedMediaList( private void postListCustomPathProcess( @NonNull List list, @NonNull MainFragment mainFragment) { - int sortType = SortHandler.getSortType(context.get(), path), sortBy, isAscending; + int sortType = SortHandler.getSortType(context.get(), path); + int sortBy; + int isAscending; if (sortType <= 3) { sortBy = sortType; From eaf8252d13af767240337467621b15b5dbedc498 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 29 May 2023 12:33:13 +0530 Subject: [PATCH 095/384] UI: move packageNameTV Signed-off-by: VishnuSanal --- .../filemanager/adapters/holders/AppHolder.kt | 9 +++++++ app/src/main/res/layout/rowlayout.xml | 26 +++++++++++-------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt index cfb5b5cca4..b14091a4c0 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt @@ -21,10 +21,14 @@ package com.amaze.filemanager.adapters.holders import android.view.View +import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.RelativeLayout import android.widget.TextView +import androidx.core.view.marginBottom +import androidx.core.view.marginLeft +import androidx.core.view.marginTop import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R import com.amaze.filemanager.ui.views.ThemedTextView @@ -54,6 +58,11 @@ class AppHolder(view: View) : RecyclerView.ViewHolder(view) { init { apkIcon.visibility = View.VISIBLE packageName.visibility = View.VISIBLE + + val layoutParams = txtDesc.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.setMargins(txtDesc.marginLeft, txtDesc.marginTop, 8, txtDesc.marginBottom) + txtDesc.layoutParams = layoutParams + view.findViewById(R.id.picture_icon).visibility = View.GONE view.findViewById(R.id.generic_icon).visibility = View.GONE } diff --git a/app/src/main/res/layout/rowlayout.xml b/app/src/main/res/layout/rowlayout.xml index 79d05255c5..b50bd98c65 100644 --- a/app/src/main/res/layout/rowlayout.xml +++ b/app/src/main/res/layout/rowlayout.xml @@ -133,21 +133,11 @@ android:clickable="false" android:textSize="17sp" /> - - + + + Date: Sun, 28 May 2023 09:08:30 +0900 Subject: [PATCH 096/384] Add InputFilter to default path to prevent illegal chars Fixes #3543. --- .../ui/dialogs/SftpConnectDialog.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 e4cbda8de7..23e95f0cd6 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 @@ -29,6 +29,7 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.text.Editable +import android.text.InputFilter import android.text.TextUtils import android.text.TextWatcher import android.view.LayoutInflater @@ -37,6 +38,7 @@ import android.widget.AdapterView import android.widget.ArrayAdapter import androidx.activity.result.contract.ActivityResultContracts import androidx.core.text.isDigitsOnly +import androidx.core.widget.addTextChangedListener import androidx.fragment.app.DialogFragment import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog @@ -104,6 +106,20 @@ class SftpConnectDialog : DialogFragment() { const val ARG_KEYPAIR_NAME = "keypairName" private val VALID_PORT_RANGE = IntRange(1, 65535) + + // Loosely referenced from https://dwheeler.com/essays/fixing-unix-linux-filenames.html + private const val pathBlockedChars = "*?<>|\\" + + private val defaultPathCharFilter = + InputFilter { source, _, _, _, _, _ -> + if (source.isNotEmpty() && source.isNotBlank() && + pathBlockedChars.contains(source) + ) { + "" + } else { + null + } + } } lateinit var ctx: WeakReference @@ -190,7 +206,7 @@ class SftpConnectDialog : DialogFragment() { passwordET.setText("") } } - + defaultPathET.filters = arrayOf(defaultPathCharFilter) // If it's new connection setup, set some default values // Otherwise, use given Bundle instance for filling in the blanks if (!edit) { From bad029ab36eb32a6cf98471dc6bca20670254d60 Mon Sep 17 00:00:00 2001 From: TranceLove Date: Wed, 31 May 2023 13:39:21 +0900 Subject: [PATCH 097/384] Fix SSH problems - fix create dir failure when copying folder structure - when using SftpClientTemplate, closeClientOnFinish flag should set to true; it only closes the SFTPClient but not the underlying SSHClient session. This should help avoid the problems with SFTPClient sessions, breaking the whole SSH session altogether --- .../asynchronous/services/CopyService.java | 4 +++- .../filemanager/filesystem/HybridFile.java | 21 ++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java index 8d831be1a3..e83fdd4af7 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java @@ -479,7 +479,9 @@ private void copyFiles( if (progressHandler.getCancelled()) return; if (sourceFile.isDirectory()) { - if (!targetFile.exists()) targetFile.mkdir(c); + if (!targetFile.exists()) { + targetFile.mkdir(c); + } // various checks // 1. source file and target file doesn't end up in loop 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 289020cdbf..8f55be3882 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -76,7 +76,6 @@ import com.amaze.filemanager.filesystem.root.ListFilesCommand; import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate; import com.amaze.filemanager.filesystem.ssh.SshClientSessionTemplate; -import com.amaze.filemanager.filesystem.ssh.SshClientTemplate; import com.amaze.filemanager.filesystem.ssh.SshClientUtils; import com.amaze.filemanager.filesystem.ssh.Statvfs; import com.amaze.filemanager.ui.activities.MainActivity; @@ -1198,14 +1197,13 @@ public OutputStream getOutputStream(Context context) { switch (mode) { case SFTP: return NetCopyClientUtils.INSTANCE.execute( - new SshClientTemplate(path, false) { + new SFtpClientTemplate(getPath(), false) { + @Nullable @Override - public OutputStream executeWithSSHClient(@NonNull final SSHClient ssh) - throws IOException { - final SFTPClient client = ssh.newSFTPClient(); + public OutputStream execute(@NonNull SFTPClient client) throws IOException { final RemoteFile rf = client.open( - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path), + NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath()), EnumSet.of( net.schmizz.sshj.sftp.OpenMode.WRITE, net.schmizz.sshj.sftp.OpenMode.CREAT)); @@ -1287,7 +1285,7 @@ public boolean exists() { if (isSftp()) { final Boolean executionReturn = NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { + new SFtpClientTemplate(path, true) { @Override public Boolean execute(SFTPClient client) throws IOException { try { @@ -1412,17 +1410,16 @@ public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) public void mkdir(Context context) { if (isSftp()) { - NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { + NetCopyClientUtils.INSTANCE.execute( + new SFtpClientTemplate(path, true) { @Override - public Void execute(@NonNull SFTPClient client) { + public Boolean execute(@NonNull SFTPClient client) { try { client.mkdir(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path)); } catch (IOException e) { LOG.error("Error making directory over SFTP", e); } - // FIXME: anything better than throwing a null to make Rx happy? - return null; + return true; } }); } else if (isFtp()) { From 160da833aa0f21a8b660edfef469310efe746775 Mon Sep 17 00:00:00 2001 From: Obolrom Date: Fri, 2 Jun 2023 20:32:24 +0300 Subject: [PATCH 098/384] Fixed #3259 --- .../amaze/filemanager/asynchronous/services/ZipService.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt index 7064a26fd2..43d4471d0a 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt @@ -65,7 +65,7 @@ class ZipService : AbstractProgressiveService() { private lateinit var asyncTask: CompressAsyncTask private lateinit var mNotifyManager: NotificationManagerCompat private lateinit var mBuilder: NotificationCompat.Builder - private lateinit var progressListener: ProgressListener + private var progressListener: ProgressListener? = null private val progressHandler = ProgressHandler() // list of data packages, to initiate chart in process viewer fragment @@ -158,9 +158,9 @@ class ZipService : AbstractProgressiveService() { override fun getNotificationCustomViewBig(): RemoteViews = customBigContentViews!! - override fun getProgressListener(): ProgressListener = progressListener + override fun getProgressListener(): ProgressListener? = progressListener - override fun setProgressListener(progressListener: ProgressListener) { + override fun setProgressListener(progressListener: ProgressListener?) { this.progressListener = progressListener } From d19344b8493b69f48a9bff43e25fd9bee889303d Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Sun, 4 Jun 2023 12:42:37 +0530 Subject: [PATCH 099/384] fixes #3830 Signed-off-by: VishnuSanal --- .../amaze/filemanager/ui/dialogs/share/ShareAdapter.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/share/ShareAdapter.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/share/ShareAdapter.java index 860865a9ed..01cb798c1f 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/share/ShareAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/share/ShareAdapter.java @@ -25,6 +25,7 @@ import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; @@ -33,6 +34,7 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import androidx.recyclerview.widget.RecyclerView; @@ -94,7 +96,11 @@ void render(final int position) { rootView.setOnClickListener( v -> { if (dialog != null && dialog.isShowing()) dialog.dismiss(); - context.startActivity(items.get(position)); + try { + context.startActivity(items.get(position)); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, R.string.no_app_found, Toast.LENGTH_SHORT).show(); + } }); } } From ddbcbe1dbae64f1f603ad77095efe50968035dc5 Mon Sep 17 00:00:00 2001 From: Obolrom Date: Sun, 4 Jun 2023 18:28:00 +0300 Subject: [PATCH 100/384] Fixed ProcessViewerFragment memory leak --- .../ui/fragments/ProcessViewerFragment.java | 256 +++++++++--------- 1 file changed, 135 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/ProcessViewerFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/ProcessViewerFragment.java index fd0c302751..b7f221da62 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/ProcessViewerFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/ProcessViewerFragment.java @@ -22,7 +22,27 @@ import static androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT; -import java.util.ArrayList; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.IBinder; +import android.text.Spanned; +import android.text.format.Formatter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.text.HtmlCompat; +import androidx.fragment.app.Fragment; import com.amaze.filemanager.R; import com.amaze.filemanager.asynchronous.services.AbstractProgressiveService; @@ -31,6 +51,7 @@ import com.amaze.filemanager.asynchronous.services.EncryptService; import com.amaze.filemanager.asynchronous.services.ExtractService; import com.amaze.filemanager.asynchronous.services.ZipService; +import com.amaze.filemanager.databinding.ProcessparentBinding; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.theme.AppTheme; @@ -45,28 +66,8 @@ import com.github.mikephil.charting.data.LineDataSet; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; -import android.content.ComponentName; -import android.content.Intent; -import android.content.ServiceConnection; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.os.IBinder; -import android.text.Spanned; -import android.text.format.Formatter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.Nullable; -import androidx.cardview.widget.CardView; -import androidx.core.text.HtmlCompat; -import androidx.fragment.app.Fragment; +import java.lang.ref.WeakReference; +import java.util.ArrayList; public class ProcessViewerFragment extends Fragment { @@ -82,21 +83,11 @@ public class ProcessViewerFragment extends Fragment { private boolean isInitialized = false; private MainActivity mainActivity; private int accentColor; - private ImageButton mCancelButton; - private ImageView mProgressImage; - private View rootView; - private CardView mCardView; - private LineChart mLineChart; - private LineData mLineData = new LineData(); + private final LineData lineData = new LineData(); + private ProcessparentBinding binding = null; /** Time in seconds just for showing to the user. No guarantees. */ private long looseTimeInSeconds = 0L; - private TextView mProgressTypeText, - mProgressFileNameText, - mProgressBytesText, - mProgressFileText, - mProgressSpeedText, - mProgressTimer; private ServiceConnection mCopyConnection, mExtractConnection, mCompressConnection, @@ -105,8 +96,9 @@ public class ProcessViewerFragment extends Fragment { @Override public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - rootView = inflater.inflate(R.layout.processparent, container, false); + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = ProcessparentBinding.inflate(inflater); + View rootView = binding.getRoot(); mainActivity = (MainActivity) getActivity(); @@ -115,40 +107,27 @@ public View onCreateView( || mainActivity.getAppTheme().equals(AppTheme.BLACK)) rootView.setBackgroundResource((R.color.cardView_background)); - mCardView = rootView.findViewById(R.id.card_view); - - mLineChart = rootView.findViewById(R.id.progress_chart); - mProgressImage = rootView.findViewById(R.id.progress_image); - mCancelButton = rootView.findViewById(R.id.delete_button); - mProgressTypeText = rootView.findViewById(R.id.text_view_progress_type); - mProgressFileNameText = rootView.findViewById(R.id.text_view_progress_file_name); - mProgressBytesText = rootView.findViewById(R.id.text_view_progress_bytes); - mProgressFileText = rootView.findViewById(R.id.text_view_progress_file); - mProgressSpeedText = rootView.findViewById(R.id.text_view_progress_speed); - mProgressTimer = rootView.findViewById(R.id.text_view_progress_timer); - if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - mCancelButton.setImageResource(R.drawable.ic_action_cancel); - mCardView.setCardBackgroundColor(Utils.getColor(getContext(), R.color.cardView_foreground)); - mCardView.setCardElevation(0f); + binding.deleteButton.setImageResource(R.drawable.ic_action_cancel); + binding.cardView.setCardBackgroundColor(Utils.getColor(getContext(), R.color.cardView_foreground)); + binding.cardView.setCardElevation(0f); } - mCopyConnection = new CustomServiceConnection(this, mLineChart, SERVICE_COPY); - mExtractConnection = new CustomServiceConnection(this, mLineChart, SERVICE_EXTRACT); - mCompressConnection = new CustomServiceConnection(this, mLineChart, SERVICE_COMPRESS); - mEncryptConnection = new CustomServiceConnection(this, mLineChart, SERVICE_ENCRYPT); - mDecryptConnection = new CustomServiceConnection(this, mLineChart, SERVICE_DECRYPT); - return rootView; } @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mCopyConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_COPY); + mExtractConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_EXTRACT); + mCompressConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_COMPRESS); + mEncryptConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_ENCRYPT); + mDecryptConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_DECRYPT); - setRetainInstance(true); mainActivity.getAppbar().setTitle(R.string.process_viewer); mainActivity.hideFab(); mainActivity.getAppbar().getBottomBar().setVisibility(View.GONE); @@ -192,7 +171,16 @@ public void onPause() { getActivity().unbindService(mDecryptConnection); } + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + mainActivity = null; + } + public void processResults(final DatapointParcelable dataPackage, int serviceType) { + if (binding == null) return; + if (dataPackage != null) { String name = dataPackage.getName(); long total = dataPackage.getTotalSize(); @@ -213,7 +201,7 @@ public void processResults(final DatapointParcelable dataPackage, int serviceTyp FileUtils.readableFileSizeFloat(doneBytes), FileUtils.readableFileSizeFloat(dataPackage.getSpeedRaw())); - mProgressFileNameText.setText(name); + binding.textViewProgressFileName.setText(name); Spanned bytesText = HtmlCompat.fromHtml( @@ -228,7 +216,7 @@ public void processResults(final DatapointParcelable dataPackage, int serviceTyp + Formatter.formatFileSize(getContext(), total) + "", FROM_HTML_MODE_COMPACT); - mProgressBytesText.setText(bytesText); + binding.textViewProgressBytes.setText(bytesText); Spanned fileProcessedSpan = HtmlCompat.fromHtml( @@ -243,7 +231,7 @@ public void processResults(final DatapointParcelable dataPackage, int serviceTyp + dataPackage.getAmountOfSourceFiles() + "", FROM_HTML_MODE_COMPACT); - mProgressFileText.setText(fileProcessedSpan); + binding.textViewProgressFile.setText(fileProcessedSpan); Spanned speedSpan = HtmlCompat.fromHtml( @@ -254,7 +242,7 @@ public void processResults(final DatapointParcelable dataPackage, int serviceTyp + Formatter.formatFileSize(getContext(), dataPackage.getSpeedRaw()) + "/s", FROM_HTML_MODE_COMPACT); - mProgressSpeedText.setText(speedSpan); + binding.textViewProgressSpeed.setText(speedSpan); Spanned timerSpan = HtmlCompat.fromHtml( @@ -266,9 +254,9 @@ public void processResults(final DatapointParcelable dataPackage, int serviceTyp + "", FROM_HTML_MODE_COMPACT); - mProgressTimer.setText(timerSpan); + binding.textViewProgressTimer.setText(timerSpan); - if (dataPackage.getCompleted()) mCancelButton.setVisibility(View.GONE); + if (dataPackage.getCompleted()) binding.deleteButton.setVisibility(View.GONE); } } @@ -279,13 +267,15 @@ private void setupDrawables(int serviceType, boolean isMove) { if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - mProgressImage.setImageDrawable( - getResources().getDrawable(R.drawable.ic_content_copy_white_36dp)); + Drawable copyIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_content_copy_white_36dp); + binding.progressImage.setImageDrawable(copyIcon); } else { - mProgressImage.setImageDrawable( - getResources().getDrawable(R.drawable.ic_content_copy_grey600_36dp)); + Drawable greyCopyIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_content_copy_grey600_36dp); + binding.progressImage.setImageDrawable(greyCopyIcon); } - mProgressTypeText.setText( + binding.textViewProgressType.setText( isMove ? getResources().getString(R.string.moving) : getResources().getString(R.string.copying)); @@ -295,48 +285,60 @@ private void setupDrawables(int serviceType, boolean isMove) { if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - mProgressImage.setImageDrawable(getResources().getDrawable(R.drawable.ic_zip_box_white)); + Drawable zipBoxIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_zip_box_white); + binding.progressImage.setImageDrawable(zipBoxIcon); } else { - mProgressImage.setImageDrawable(getResources().getDrawable(R.drawable.ic_zip_box_grey)); + Drawable greyZipBoxIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_zip_box_grey); + binding.progressImage.setImageDrawable(greyZipBoxIcon); } - mProgressTypeText.setText(getResources().getString(R.string.extracting)); + binding.textViewProgressType.setText(getResources().getString(R.string.extracting)); cancelBroadcast(new Intent(ExtractService.TAG_BROADCAST_EXTRACT_CANCEL)); break; case SERVICE_COMPRESS: if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - mProgressImage.setImageDrawable(getResources().getDrawable(R.drawable.ic_zip_box_white)); + Drawable zipBoxIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_zip_box_white); + binding.progressImage.setImageDrawable(zipBoxIcon); } else { - mProgressImage.setImageDrawable(getResources().getDrawable(R.drawable.ic_zip_box_grey)); + Drawable greyZipBoxIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_zip_box_grey); + binding.progressImage.setImageDrawable(greyZipBoxIcon); } - mProgressTypeText.setText(getResources().getString(R.string.compressing)); + binding.textViewProgressType.setText(getResources().getString(R.string.compressing)); cancelBroadcast(new Intent(ZipService.KEY_COMPRESS_BROADCAST_CANCEL)); break; case SERVICE_ENCRYPT: if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - mProgressImage.setImageDrawable( - getResources().getDrawable(R.drawable.ic_folder_lock_white_36dp)); + Drawable folderIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_folder_lock_white_36dp); + binding.progressImage.setImageDrawable(folderIcon); } else { - mProgressImage.setImageDrawable( - getResources().getDrawable(R.drawable.ic_folder_lock_grey600_36dp)); + Drawable greyFolderIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_folder_lock_grey600_36dp); + binding.progressImage.setImageDrawable(greyFolderIcon); } - mProgressTypeText.setText(getResources().getString(R.string.crypt_encrypting)); + binding.textViewProgressType.setText(getResources().getString(R.string.crypt_encrypting)); cancelBroadcast(new Intent(EncryptService.TAG_BROADCAST_CRYPT_CANCEL)); break; case SERVICE_DECRYPT: if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - mProgressImage.setImageDrawable( - getResources().getDrawable(R.drawable.ic_folder_lock_open_white_36dp)); + Drawable folderUnlockedIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_folder_lock_open_white_36dp); + binding.progressImage.setImageDrawable(folderUnlockedIcon); } else { - mProgressImage.setImageDrawable( - getResources().getDrawable(R.drawable.ic_folder_lock_open_grey600_36dp)); + Drawable greyFolderUnlockedIcon = ContextCompat + .getDrawable(requireContext(), R.drawable.ic_folder_lock_open_grey600_36dp); + binding.progressImage.setImageDrawable(greyFolderUnlockedIcon); } - mProgressTypeText.setText(getResources().getString(R.string.crypt_decrypting)); + binding.textViewProgressType.setText(getResources().getString(R.string.crypt_decrypting)); cancelBroadcast(new Intent(EncryptService.TAG_BROADCAST_CRYPT_CANCEL)); break; } @@ -344,20 +346,21 @@ private void setupDrawables(int serviceType, boolean isMove) { /** Setup click listener to cancel button click for various intent types */ private void cancelBroadcast(final Intent intent) { + if (binding == null) return; - mCancelButton.setOnClickListener( + binding.deleteButton.setOnClickListener( v -> { Toast.makeText( getActivity(), getResources().getString(R.string.stopping), Toast.LENGTH_LONG) .show(); getActivity().sendBroadcast(intent); - mProgressTypeText.setText(getResources().getString(R.string.cancelled)); - mProgressSpeedText.setText(""); - mProgressFileText.setText(""); - mProgressBytesText.setText(""); - mProgressFileNameText.setText(""); + binding.textViewProgressType.setText(getResources().getString(R.string.cancelled)); + binding.textViewProgressSpeed.setText(""); + binding.textViewProgressFile.setText(""); + binding.textViewProgressBytes.setText(""); + binding.textViewProgressFileName.setText(""); - mProgressTypeText.setTextColor( + binding.textViewProgressType.setTextColor( Utils.getColor(getContext(), android.R.color.holo_red_light)); }); } @@ -369,18 +372,18 @@ private void cancelBroadcast(final Intent intent) { * @param yValue the y-axis value, bytes processed per sec */ private void addEntry(float xValue, float yValue) { - ILineDataSet dataSet = mLineData.getDataSetByIndex(0); + ILineDataSet dataSet = lineData.getDataSetByIndex(0); if (dataSet == null) { // adding set for first time dataSet = createDataSet(); - mLineData.addDataSet(dataSet); + lineData.addDataSet(dataSet); } dataSet.addEntry(new Entry(xValue, yValue)); - mLineData.notifyDataChanged(); - mLineChart.notifyDataSetChanged(); - mLineChart.invalidate(); + lineData.notifyDataChanged(); + binding.progressChart.notifyDataSetChanged(); + binding.progressChart.invalidate(); } /** Creates an instance for {@link LineDataSet} which will store the entries */ @@ -405,15 +408,15 @@ private LineDataSet createDataSet() { * @param totalBytes maximum value for x-axis */ private void chartInit(long totalBytes) { - mLineChart.setBackgroundColor(accentColor); - mLineChart.getLegend().setEnabled(false); + binding.progressChart.setBackgroundColor(accentColor); + binding.progressChart.getLegend().setEnabled(false); // no description text - mLineChart.getDescription().setEnabled(false); + binding.progressChart.getDescription().setEnabled(false); - XAxis xAxis = mLineChart.getXAxis(); - YAxis yAxisLeft = mLineChart.getAxisLeft(); - mLineChart.getAxisRight().setEnabled(false); + XAxis xAxis = binding.progressChart.getXAxis(); + YAxis yAxisLeft = binding.progressChart.getAxisLeft(); + binding.progressChart.getAxisRight().setEnabled(false); yAxisLeft.setTextColor(Color.WHITE); yAxisLeft.setAxisLineColor(Color.TRANSPARENT); yAxisLeft.setTypeface(Typeface.DEFAULT_BOLD); @@ -425,20 +428,20 @@ private void chartInit(long totalBytes) { xAxis.setGridColor(Color.TRANSPARENT); xAxis.setTextColor(Color.WHITE); xAxis.setTypeface(Typeface.DEFAULT_BOLD); - mLineChart.setData(mLineData); - mLineChart.invalidate(); + binding.progressChart.setData(lineData); + binding.progressChart.invalidate(); } private static class CustomServiceConnection implements ServiceConnection { - private ProcessViewerFragment fragment; - private LineChart lineChart; - private int serviceType; + private final WeakReference fragment; + private final WeakReference lineChart; + private final int serviceType; public CustomServiceConnection( ProcessViewerFragment frag, LineChart lineChart, int serviceType) { - fragment = frag; - this.lineChart = lineChart; + fragment = new WeakReference<>(frag); + this.lineChart = new WeakReference<>(lineChart); this.serviceType = serviceType; } @@ -450,23 +453,34 @@ public void onServiceConnected(ComponentName name, IBinder service) { for (int i = 0; i < specificService.getDataPackageSize(); i++) { DatapointParcelable dataPackage = specificService.getDataPackage(i); - fragment.processResults(dataPackage, serviceType); + ProcessViewerFragment processViewerFragment = fragment.get(); + if (processViewerFragment != null) { + processViewerFragment.processResults(dataPackage, serviceType); + } } // animate the chart a little after initial values have been applied - lineChart.animateXY(500, 500); + LineChart chart = lineChart.get(); + if (chart != null) { + chart.animateXY(500, 500); + } specificService.setProgressListener( new AbstractProgressiveService.ProgressListener() { @Override public void onUpdate(final DatapointParcelable dataPackage) { - if (fragment.getActivity() == null) { - // callback called when we're not inside the app - return; + ProcessViewerFragment processViewerFragment = fragment.get(); + if (processViewerFragment != null) { + if (processViewerFragment.getActivity() == null) { + // callback called when we're not inside the app + return; + } + processViewerFragment + .getActivity() + .runOnUiThread(() -> + processViewerFragment.processResults(dataPackage, serviceType) + ); } - fragment - .getActivity() - .runOnUiThread(() -> fragment.processResults(dataPackage, serviceType)); } @Override From 5eeef32dbb9cf4854296943e85e2f0f7185dffc4 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 24 Sep 2022 17:39:48 +0800 Subject: [PATCH 101/384] Subnetscanner v2 Fixes #2622 and fixes #3386. --- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 3 +- .../asynchronous/services/ftp/FtpService.kt | 110 +----- .../services/ftp/FtpTileService.java | 5 +- .../filemanager/filesystem/FileUtil.java | 2 +- .../filemanager/filesystem/HybridFile.java | 2 +- .../filesystem/ftp/NetCopyClientUtils.kt | 2 +- .../ui/dialogs/SmbConnectDialog.java | 14 +- .../ui/dialogs/SmbSearchDialog.java | 236 ------------- .../filemanager/ui/dialogs/SmbSearchDialog.kt | 256 ++++++++++++++ .../ui/fragments/CloudSheetFragment.java | 2 +- .../ui/fragments/FtpServerFragment.kt | 6 +- .../ui/notifications/FtpNotification.java | 3 +- .../filemanager/utils/ComputerParcelable.kt | 7 +- .../filemanager/utils/MainActivityHelper.java | 1 + .../amaze/filemanager/utils/NetworkUtil.kt | 151 +++++++++ .../filemanager/utils/SubnetScanner.java | 252 -------------- .../com/amaze/filemanager/utils/UUIDv5.kt | 94 ++++++ .../smb/SameSubnetDiscoverDeviceStrategy.kt | 102 ++++++ .../utils/smb/SmbDeviceScannerObservable.kt | 96 ++++++ .../filemanager/utils/{ => smb}/SmbUtil.kt | 4 +- .../utils/smb/WsddDiscoverDeviceStrategy.kt | 294 ++++++++++++++++ app/src/main/res/raw/wsd_request.txt | 22 ++ app/src/main/res/raw/wsdd_discovery.txt | 1 + .../asynctasks/SmbDeleteTaskTest.kt | 2 +- .../UtilitiesDatabaseMigrationTest.kt | 2 +- .../filemanager/database/UtilsHandlerTest.kt | 2 +- .../utils/ComputerParcelableTest.java | 6 +- .../amaze/filemanager/utils/SmbUtilTest.kt | 8 +- .../com/amaze/filemanager/utils/UUIDv5Test.kt | 43 +++ ...tractSubnetDiscoverDevicesStrategyTests.kt | 75 +++++ .../SameSubnetDiscoverDevicesStrategyTest.kt | 58 ++++ .../WsddSubnetDiscoverDevicesStrategyTest.kt | 156 +++++++++ .../resources/wsdd/multicast-response.txt | 28 ++ app/src/test/resources/wsdd/wsd-response.txt | 46 +++ build.gradle | 4 +- portscanner/.gitignore | 1 + portscanner/build.gradle | 37 ++ portscanner/consumer-rules.pro | 0 portscanner/proguard-rules.pro | 21 ++ portscanner/src/main/AndroidManifest.xml | 8 + .../com/stealthcopter/networktools/IPTools.kt | 130 ++++++++ .../stealthcopter/networktools/PortScan.kt | 315 ++++++++++++++++++ .../networktools/portscanning/PortScanTCP.kt | 50 +++ .../networktools/portscanning/PortScanUDP.kt | 57 ++++ settings.gradle | 1 + .../filemanager/shadows/ShadowSmbUtil.kt | 2 +- .../filemanager/test/volley/MockHttpStack.kt | 51 +++ 48 files changed, 2141 insertions(+), 633 deletions(-) delete mode 100644 app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.java create mode 100644 app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt create mode 100644 app/src/main/java/com/amaze/filemanager/utils/NetworkUtil.kt delete mode 100644 app/src/main/java/com/amaze/filemanager/utils/SubnetScanner.java create mode 100644 app/src/main/java/com/amaze/filemanager/utils/UUIDv5.kt create mode 100644 app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt create mode 100644 app/src/main/java/com/amaze/filemanager/utils/smb/SmbDeviceScannerObservable.kt rename app/src/main/java/com/amaze/filemanager/utils/{ => smb}/SmbUtil.kt (98%) create mode 100644 app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt create mode 100644 app/src/main/res/raw/wsd_request.txt create mode 100644 app/src/main/res/raw/wsdd_discovery.txt create mode 100644 app/src/test/java/com/amaze/filemanager/utils/UUIDv5Test.kt create mode 100644 app/src/test/java/com/amaze/filemanager/utils/smb/AbstractSubnetDiscoverDevicesStrategyTests.kt create mode 100644 app/src/test/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDevicesStrategyTest.kt create mode 100644 app/src/test/java/com/amaze/filemanager/utils/smb/WsddSubnetDiscoverDevicesStrategyTest.kt create mode 100644 app/src/test/resources/wsdd/multicast-response.txt create mode 100644 app/src/test/resources/wsdd/wsd-response.txt create mode 100644 portscanner/.gitignore create mode 100644 portscanner/build.gradle create mode 100644 portscanner/consumer-rules.pro create mode 100644 portscanner/proguard-rules.pro create mode 100644 portscanner/src/main/AndroidManifest.xml create mode 100644 portscanner/src/main/java/com/stealthcopter/networktools/IPTools.kt create mode 100644 portscanner/src/main/java/com/stealthcopter/networktools/PortScan.kt create mode 100644 portscanner/src/main/java/com/stealthcopter/networktools/portscanning/PortScanTCP.kt create mode 100644 portscanner/src/main/java/com/stealthcopter/networktools/portscanning/PortScanUDP.kt create mode 100644 testShared/src/test/java/com/amaze/filemanager/test/volley/MockHttpStack.kt diff --git a/app/build.gradle b/app/build.gradle index 34cd3e4b97..2e1f2660f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,7 +133,6 @@ dependencies { implementation "androidx.room:room-rxjava2:$roomVersion" kapt "androidx.annotation:annotation:$androidXAnnotationVersion" - implementation "androidx.preference:preference:$androidXPrefVersion" implementation "androidx.preference:preference-ktx:$androidXPrefVersion" //For tests @@ -233,14 +232,15 @@ dependencies { implementation 'org.tukaani:xz:1.9' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" // Because RxAndroid releases are few and far between, it is recommended you also // explicitly depend on RxJava's latest version for bug fixes and new features. // (see https://github.com/ReactiveX/RxJava/releases for latest 3.x.x version) - implementation group: 'io.reactivex.rxjava2', name: 'rxjava', version: '2.2.9' + implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" implementation project(':commons_compress_7z') implementation project(':file_operations') + implementation project(':portscanner') implementation "androidx.core:core-ktx:$androidXCoreVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "ch.acra:acra-core:5.7.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3eb63f4c86..96ff20b8a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,7 +54,8 @@ tools:replace="android:label" android:label="@string/appbar_name" android:requestLegacyExternalStorage="true" - android:banner="@drawable/about_header"> + android:banner="@drawable/about_header" + android:usesCleartextTraffic="true"> = M) { - connected = cm.activeNetwork?.let { activeNetwork -> - cm.getNetworkCapabilities(activeNetwork)?.let { ni -> - ni.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) or - ni.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) - } ?: false - } ?: false - } else { - connected = cm.activeNetworkInfo?.let { ni -> - ni.isConnected && ( - ni.type and ( - ConnectivityManager.TYPE_WIFI - or ConnectivityManager.TYPE_ETHERNET - ) != 0 - ) - } ?: false - } - - if (!connected) { - connected = runCatching { - NetworkInterface.getNetworkInterfaces().toList().find { netInterface -> - netInterface.displayName.startsWith("rndis") or - netInterface.displayName.startsWith("wlan") - } - }.getOrElse { null } != null - } - - return connected - } - - /** - * Is the device connected to Wifi? - */ - @JvmStatic - fun isConnectedToWifi(context: Context): Boolean { - val cm = context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager - return if (SDK_INT >= M) { - cm.activeNetwork?.let { - cm.getNetworkCapabilities(it)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) - } ?: false - } else { - cm.activeNetworkInfo?.let { - it.isConnected && it.type == ConnectivityManager.TYPE_WIFI - } ?: false - } - } - - /** - * Determine device's IP address - */ - @JvmStatic - fun getLocalInetAddress(context: Context): InetAddress? { - if (!isConnectedToLocalNetwork(context)) { - return null - } - if (isConnectedToWifi(context)) { - val wm = context.applicationContext.getSystemService(WIFI_SERVICE) as WifiManager - val ipAddress = wm.connectionInfo.ipAddress - return if (ipAddress == 0) null else intToInet(ipAddress) - } - runCatching { - NetworkInterface.getNetworkInterfaces().iterator().forEach { netinterface -> - netinterface.inetAddresses.iterator().forEach { address -> - // this is the condition that sometimes gives problems - if (!address.isLoopbackAddress && - !address.isLinkLocalAddress - ) { - return address - } - } - } - }.onFailure { e -> - log.warn("failed to get local inet address", e) - } - return null - } - - private fun intToInet(value: Int): InetAddress? { - val bytes = ByteArray(4) - for (i in 0..3) { - bytes[i] = byteOfInt(value, i) - } - return try { - InetAddress.getByAddress(bytes) - } catch (e: UnknownHostException) { - // This only happens if the byte array has a bad length - null - } - } - - private fun byteOfInt(value: Int, which: Int): Byte { - val shift = which * 8 - return (value shr shift).toByte() - } - private fun getPort(preferences: SharedPreferences): Int { - return preferences.getInt(PORT_PREFERENCE_KEY, DEFAULT_PORT) + return preferences.getInt(FtpService.PORT_PREFERENCE_KEY, FtpService.DEFAULT_PORT) } } } diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ftp/FtpTileService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ftp/FtpTileService.java index c9c44158f8..01e74981b3 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ftp/FtpTileService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ftp/FtpTileService.java @@ -24,6 +24,7 @@ import org.greenrobot.eventbus.Subscribe; import com.amaze.filemanager.R; +import com.amaze.filemanager.utils.NetworkUtil; import android.annotation.TargetApi; import android.content.Intent; @@ -64,8 +65,8 @@ public void onClick() { .sendBroadcast( new Intent(FtpService.ACTION_STOP_FTPSERVER).setPackage(getPackageName())); } else { - if (FtpService.isConnectedToWifi(getApplicationContext()) - || FtpService.isConnectedToLocalNetwork(getApplicationContext())) { + if (NetworkUtil.isConnectedToWifi(getApplicationContext()) + || NetworkUtil.isConnectedToLocalNetwork(getApplicationContext())) { Intent i = new Intent(FtpService.ACTION_START_FTPSERVER).setPackage(getPackageName()); i.putExtra(FtpService.TAG_STARTED_BY_TILE, true); getApplicationContext().sendBroadcast(i); diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java b/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java index cd4bdec12b..3d129a9fdc 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java @@ -43,7 +43,7 @@ import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; -import com.amaze.filemanager.utils.SmbUtil; +import com.amaze.filemanager.utils.smb.SmbUtil; import com.cloudrail.si.interfaces.CloudStorage; import android.content.ContentResolver; 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 8f55be3882..2f91894d0d 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -85,8 +85,8 @@ import com.amaze.filemanager.utils.Function; import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.OnFileFound; -import com.amaze.filemanager.utils.SmbUtil; import com.amaze.filemanager.utils.Utils; +import com.amaze.filemanager.utils.smb.SmbUtil; import com.cloudrail.si.interfaces.CloudStorage; import com.cloudrail.si.types.SpaceAllocation; diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt index 372bc5279f..3fedf1b0eb 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientUtils.kt @@ -38,7 +38,7 @@ import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLO import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.SLASH import com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate -import com.amaze.filemanager.utils.SmbUtil +import com.amaze.filemanager.utils.smb.SmbUtil import com.amaze.filemanager.utils.urlEncoded import io.reactivex.Maybe import io.reactivex.Scheduler diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialog.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialog.java index 50f30fd136..b05b08e259 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialog.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialog.java @@ -25,7 +25,7 @@ import static com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.COLON; import static com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.SLASH; import static com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX; -import static com.amaze.filemanager.utils.SmbUtil.PARAM_DISABLE_IPC_SIGNING_CHECK; +import static com.amaze.filemanager.utils.smb.SmbUtil.PARAM_DISABLE_IPC_SIGNING_CHECK; import static java.net.URLDecoder.decode; import static java.net.URLEncoder.encode; @@ -49,8 +49,8 @@ import com.amaze.filemanager.utils.EditTextColorStateUtil; import com.amaze.filemanager.utils.PasswordUtil; import com.amaze.filemanager.utils.SimpleTextWatcher; -import com.amaze.filemanager.utils.SmbUtil; import com.amaze.filemanager.utils.Utils; +import com.amaze.filemanager.utils.smb.SmbUtil; import com.google.android.material.textfield.TextInputLayout; import android.app.Dialog; @@ -227,15 +227,15 @@ public void afterTextChanged(@NonNull Editable s) { final AppCompatCheckBox chkSmbDisableIpcSignature = binding.chkSmbDisableIpcSignature; TextView help = binding.wanthelp; - EditTextColorStateUtil.setTint(context, conName, accentColor); - EditTextColorStateUtil.setTint(context, user, accentColor); - EditTextColorStateUtil.setTint(context, pass, accentColor); + EditTextColorStateUtil.setTint(getActivity(), conName, accentColor); + EditTextColorStateUtil.setTint(getActivity(), user, accentColor); + EditTextColorStateUtil.setTint(getActivity(), pass, accentColor); - Utils.setTint(context, chkSmbAnonymous, accentColor); + Utils.setTint(getActivity(), chkSmbAnonymous, accentColor); help.setOnClickListener( v -> { int accentColor1 = ((ThemedActivity) getActivity()).getAccent(); - GeneralDialogCreation.showSMBHelpDialog(context, accentColor1); + GeneralDialogCreation.showSMBHelpDialog(getActivity(), accentColor1); }); chkSmbAnonymous.setOnClickListener( diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.java deleted file mode 100644 index ee0626834f..0000000000 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.java +++ /dev/null @@ -1,236 +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.ui.dialogs; - -import java.util.ArrayList; -import java.util.List; - -import com.afollestad.materialdialogs.MaterialDialog; -import com.amaze.filemanager.R; -import com.amaze.filemanager.ui.activities.MainActivity; -import com.amaze.filemanager.ui.activities.superclasses.BasicActivity; -import com.amaze.filemanager.ui.activities.superclasses.ThemedActivity; -import com.amaze.filemanager.ui.provider.UtilitiesProvider; -import com.amaze.filemanager.ui.theme.AppTheme; -import com.amaze.filemanager.utils.ComputerParcelable; -import com.amaze.filemanager.utils.SubnetScanner; - -import android.app.Activity; -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.Context; -import android.graphics.Color; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -/** Created by arpitkh996 on 16-01-2016 edited by Emmanuel Messulam */ -public class SmbSearchDialog extends DialogFragment { - private UtilitiesProvider utilsProvider; - - private ListViewAdapter listViewAdapter; - private ArrayList computers = new ArrayList<>(); - private int accentColor; - private SubnetScanner subnetScanner; - - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - utilsProvider = ((BasicActivity) getActivity()).getUtilsProvider(); - - accentColor = ((ThemedActivity) getActivity()).getAccent(); - } - - @Override - public void dismiss() { - super.dismiss(); - if (subnetScanner != null) subnetScanner.cancel(true); - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - MaterialDialog.Builder builder = new MaterialDialog.Builder(getActivity()); - builder.title(R.string.searching_devices); - builder.negativeColor(accentColor); - builder.negativeText(R.string.cancel); - builder.onNegative( - (dialog, which) -> { - if (subnetScanner != null) subnetScanner.cancel(true); - dismiss(); - }); - builder.onPositive( - (dialog, which) -> { - if (subnetScanner != null) subnetScanner.cancel(true); - if (getActivity() != null && getActivity() instanceof MainActivity) { - dismiss(); - MainActivity mainActivity = (MainActivity) getActivity(); - mainActivity.showSMBDialog("", "", false); - } - }); - builder.positiveText(R.string.use_custom_ip); - builder.positiveColor(accentColor); - computers.add(new ComputerParcelable("-1", "-1")); - listViewAdapter = new ListViewAdapter(getActivity(), computers); - subnetScanner = new SubnetScanner(getActivity()); - subnetScanner.setObserver( - new SubnetScanner.ScanObserver() { - @Override - public void computerFound(final ComputerParcelable computer) { - if (getActivity() != null) - getActivity() - .runOnUiThread( - () -> { - if (!computers.contains(computer)) computers.add(computer); - listViewAdapter.notifyDataSetChanged(); - }); - } - - @Override - public void searchFinished() { - if (getActivity() != null) { - getActivity() - .runOnUiThread( - () -> { - if (computers.size() == 1) { - dismiss(); - Toast.makeText( - getActivity(), - getString(R.string.no_device_found), - Toast.LENGTH_SHORT) - .show(); - MainActivity mainActivity = (MainActivity) getActivity(); - mainActivity.showSMBDialog("", "", false); - return; - } - computers.remove(computers.size() - 1); - listViewAdapter.notifyDataSetChanged(); - }); - } - } - }); - subnetScanner.execute(); - - builder.adapter(listViewAdapter, null); - return builder.build(); - } - - private class ListViewAdapter extends RecyclerView.Adapter { - private static final int VIEW_PROGRESSBAR = 1; - private static final int VIEW_ELEMENT = 2; - - private ArrayList items; - private LayoutInflater mInflater; - - ListViewAdapter(Context context, List objects) { - items = new ArrayList<>(objects); - mInflater = (LayoutInflater) context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE); - } - - @Override - @NonNull - public ElementViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view; - switch (viewType) { - case VIEW_PROGRESSBAR: - view = mInflater.inflate(R.layout.smb_progress_row, parent, false); - return new ElementViewHolder(view); - default: - case VIEW_ELEMENT: - view = mInflater.inflate(R.layout.smb_computers_row, parent, false); - return new ElementViewHolder(view); - } - } - - @Override - public void onBindViewHolder(@NonNull ElementViewHolder holder, int position) { - int viewType = getItemViewType(position); - if (viewType == VIEW_PROGRESSBAR) { - return; - } - - ComputerParcelable f = items.get(position); - - holder.rootView.setOnClickListener( - v -> { - if (subnetScanner != null) subnetScanner.cancel(true); - if (getActivity() != null && getActivity() instanceof MainActivity) { - dismiss(); - MainActivity mainActivity = (MainActivity) getActivity(); - mainActivity.showSMBDialog( - listViewAdapter.items.get(position).getName(), - listViewAdapter.items.get(position).getAddr(), - false); - } - }); - - holder.txtTitle.setText(f.getName()); - holder.image.setImageResource(R.drawable.ic_settings_remote_white_48dp); - if (utilsProvider.getAppTheme().equals(AppTheme.LIGHT)) - holder.image.setColorFilter(Color.parseColor("#666666")); - holder.txtDesc.setText(f.getAddr()); - } - - @Override - public int getItemViewType(int position) { - ComputerParcelable f = items.get(position); - if (f.getAddr().equals("-1")) { - return VIEW_PROGRESSBAR; - } else { - return VIEW_ELEMENT; - } - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public int getItemCount() { - return items.size(); - } - } - - private static class ElementViewHolder extends RecyclerView.ViewHolder { - private View rootView; - - private ImageView image; - private TextView txtTitle; - private TextView txtDesc; - - ElementViewHolder(View view) { - super(view); - - rootView = view; - - txtTitle = view.findViewById(R.id.firstline); - image = view.findViewById(R.id.icon); - txtDesc = view.findViewById(R.id.secondLine); - } - } -} diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt new file mode 100644 index 0000000000..bd92520db8 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt @@ -0,0 +1,256 @@ +/* + * 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.ui.dialogs + +import android.app.Activity +import android.app.Dialog +import android.content.Context +import android.graphics.Color +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.recyclerview.widget.RecyclerView +import com.afollestad.materialdialogs.DialogAction +import com.afollestad.materialdialogs.MaterialDialog +import com.amaze.filemanager.R +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.ui.activities.superclasses.ThemedActivity +import com.amaze.filemanager.ui.provider.UtilitiesProvider +import com.amaze.filemanager.ui.theme.AppTheme +import com.amaze.filemanager.utils.ComputerParcelable +import com.amaze.filemanager.utils.smb.SmbDeviceScannerObservable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.slf4j.LoggerFactory + +/** Created by arpitkh996 on 16-01-2016 edited by Emmanuel Messulam @gmail.com> */ +class SmbSearchDialog : DialogFragment() { + + private lateinit var utilsProvider: UtilitiesProvider + private lateinit var listViewAdapter: ListViewAdapter + private val viewModel = ComputerParcelableViewModel() + private var accentColor = 0 + private lateinit var subnetScannerObserver: Disposable + + override fun onCreate(bundle: Bundle?) { + super.onCreate(bundle) + utilsProvider = AppConfig.getInstance().utilsProvider + accentColor = (activity as ThemedActivity).accent + } + + override fun dismiss() { + super.dismiss() + if (!subnetScannerObserver.isDisposed) { + subnetScannerObserver.dispose() + } + } + + @Suppress("LongMethod", "LabeledExpression") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val builder = MaterialDialog.Builder(requireActivity()) + builder.title(R.string.searching_devices) + builder.negativeColor(accentColor) + builder.negativeText(R.string.cancel) + builder.onNegative { _: MaterialDialog?, _: DialogAction? -> + if (!subnetScannerObserver.isDisposed) { + subnetScannerObserver.dispose() + } + dismiss() + } + builder.onPositive { _: MaterialDialog?, _: DialogAction? -> + if (!subnetScannerObserver.isDisposed) { + subnetScannerObserver.dispose() + } + if (activity != null && activity is MainActivity) { + dismiss() + val mainActivity = activity as MainActivity + mainActivity.showSMBDialog("", "", false) + } + } + builder.positiveText(R.string.use_custom_ip) + builder.positiveColor(accentColor) + viewModel.valHolder.value = (ComputerParcelable("-1", "-1")) + listViewAdapter = ListViewAdapter(requireActivity()) + val observable = SmbDeviceScannerObservable() + subnetScannerObserver = observable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnDispose { + observable.stop() + } + .subscribe( + { computer: ComputerParcelable -> + if (!listViewAdapter.contains(computer)) { + viewModel.valHolder.value = computer + } + }, + { err: Throwable -> + LOG.error("Error searching for devices", err) + } + ) { + subnetScannerObserver.dispose() + activity?.runOnUiThread { + if (listViewAdapter.dummyOnly()) { + dismiss() + Toast.makeText( + activity, + getString(R.string.no_device_found), + Toast.LENGTH_SHORT + ).show() + val mainActivity = activity as MainActivity + mainActivity.showSMBDialog("", "", false) + return@runOnUiThread + } + listViewAdapter.removeDummy() + } + } + builder.adapter(listViewAdapter, null) + viewModel.valHolder.observe(this) { + listViewAdapter.add(it) + } + return builder.build() + } + + private inner class ListViewAdapter( + context: Context + ) : RecyclerView.Adapter() { + private val items: MutableList = ArrayList() + private val mInflater: LayoutInflater + + init { + mInflater = context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater + } + + /** + * Called by [ComputerParcelableViewModel], add found computer to list view + */ + fun add(computer: ComputerParcelable) { + items.add(computer) + notifyDataSetChanged() + } + + /** + * Called by Observable when finish probing. If no other computers found, remove first + * (dummy) host + */ + fun removeDummy() { + items.removeFirst() + notifyDataSetChanged() + } + + /** + * Answers if the computer list contains given instance. + */ + fun contains(computer: ComputerParcelable): Boolean { + return items.contains(computer) + } + + /** + * Answers if the list is empty = only has the dummy [ComputerParcelable] instance + */ + fun dummyOnly(): Boolean { + return items.size == 1 && items.first().addr == "-1" + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view: View + return when (viewType) { + VIEW_PROGRESSBAR -> { + view = mInflater.inflate(R.layout.smb_progress_row, parent, false) + ViewHolder(view) + } + else -> { + view = + mInflater.inflate(R.layout.smb_computers_row, parent, false) + ElementViewHolder(view) + } + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val viewType = getItemViewType(position) + if (viewType == Companion.VIEW_PROGRESSBAR) { + return + } + val (addr, name) = items[position] + holder.rootView.setOnClickListener { + if (activity != null && activity is MainActivity) { + dismiss() + val mainActivity = activity as MainActivity + mainActivity.showSMBDialog( + listViewAdapter.items[position].name, + listViewAdapter.items[position].addr, + false + ) + } + } + if (holder is ElementViewHolder) { + holder.txtTitle.text = name + holder.image.setImageResource(R.drawable.ic_settings_remote_white_48dp) + if (utilsProvider.appTheme == AppTheme.LIGHT) { + holder.image.setColorFilter(Color.parseColor("#666666")) + } + holder.txtDesc.text = addr + } + } + + override fun getItemViewType(position: Int): Int { + val (addr) = items[position] + return if (addr == "-1") { + VIEW_PROGRESSBAR + } else { + VIEW_ELEMENT + } + } + + override fun getItemId(position: Int): Long = position.toLong() + + override fun getItemCount(): Int = items.size + } + + private open class ViewHolder(val rootView: View) : RecyclerView.ViewHolder(rootView) + + private class ElementViewHolder(rootView: View) : + ViewHolder(rootView) { + val image: ImageView = rootView.findViewById(R.id.icon) + val txtTitle: TextView = rootView.findViewById(R.id.firstline) + val txtDesc: TextView = rootView.findViewById(R.id.secondLine) + } + + private class ComputerParcelableViewModel : ViewModel() { + val valHolder = MutableLiveData() + } + + companion object { + private const val VIEW_PROGRESSBAR = 1 + private const val VIEW_ELEMENT = 2 + private val LOG = LoggerFactory.getLogger(SmbSearchDialog::class.java) + } +} diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java index 648abc8b3c..0037e97a49 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java @@ -159,7 +159,7 @@ public void onClick(View v) { case R.id.linear_layout_smb: dismiss(); SmbSearchDialog smbDialog = new SmbSearchDialog(); - smbDialog.show(getActivity().getFragmentManager(), "tab"); + smbDialog.show(getActivity().getSupportFragmentManager(), "tab"); return; case R.id.linear_layout_scp: dismiss(); diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt index 31545e7b88..9cde4e5cc7 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/FtpServerFragment.kt @@ -68,9 +68,6 @@ import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.asynchronous.services.ftp.FtpService import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.KEY_PREFERENCE_PATH import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.KEY_PREFERENCE_ROOT_FILESYSTEM -import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.getLocalInetAddress -import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.isConnectedToLocalNetwork -import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.isConnectedToWifi import com.amaze.filemanager.asynchronous.services.ftp.FtpService.Companion.isRunning import com.amaze.filemanager.asynchronous.services.ftp.FtpService.FtpReceiverActions import com.amaze.filemanager.databinding.DialogFtpLoginBinding @@ -80,6 +77,9 @@ import com.amaze.filemanager.ui.activities.MainActivity import com.amaze.filemanager.ui.notifications.FtpNotification import com.amaze.filemanager.ui.runIfDocumentsUIExists import com.amaze.filemanager.ui.theme.AppTheme +import com.amaze.filemanager.utils.NetworkUtil.getLocalInetAddress +import com.amaze.filemanager.utils.NetworkUtil.isConnectedToLocalNetwork +import com.amaze.filemanager.utils.NetworkUtil.isConnectedToWifi import com.amaze.filemanager.utils.OneCharacterCharSequence import com.amaze.filemanager.utils.PasswordUtil import com.amaze.filemanager.utils.Utils diff --git a/app/src/main/java/com/amaze/filemanager/ui/notifications/FtpNotification.java b/app/src/main/java/com/amaze/filemanager/ui/notifications/FtpNotification.java index d803267ff7..defb81ff26 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/notifications/FtpNotification.java +++ b/app/src/main/java/com/amaze/filemanager/ui/notifications/FtpNotification.java @@ -28,6 +28,7 @@ import com.amaze.filemanager.R; import com.amaze.filemanager.asynchronous.services.ftp.FtpService; import com.amaze.filemanager.ui.activities.MainActivity; +import com.amaze.filemanager.utils.NetworkUtil; import android.app.Notification; import android.app.NotificationManager; @@ -104,7 +105,7 @@ public static void updateNotification(Context context, boolean noStopButton) { boolean secureConnection = sharedPreferences.getBoolean(FtpService.KEY_PREFERENCE_SECURE, FtpService.DEFAULT_SECURE); - InetAddress address = FtpService.getLocalInetAddress(context); + InetAddress address = NetworkUtil.getLocalInetAddress(context); String address_text = "Address not found"; diff --git a/app/src/main/java/com/amaze/filemanager/utils/ComputerParcelable.kt b/app/src/main/java/com/amaze/filemanager/utils/ComputerParcelable.kt index b5358507f1..4b8a6eeac6 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/ComputerParcelable.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/ComputerParcelable.kt @@ -24,6 +24,9 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class ComputerParcelable(val addr: String?, val name: String?) : Parcelable { - override fun toString(): String = String.format("%s [%s]", name, addr) +data class ComputerParcelable(val addr: String, val name: String) : Parcelable { + override fun toString(): String = "$name [$addr]" + override fun hashCode(): Int = addr.hashCode() + override fun equals(other: Any?): Boolean = + other is ComputerParcelable && other.addr == this.addr } diff --git a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java index 53a029c69e..9caba5adb9 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java @@ -66,6 +66,7 @@ import com.amaze.filemanager.ui.fragments.TabFragment; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.views.WarnableTextInputValidator; +import com.amaze.filemanager.utils.smb.SmbUtil; import com.leinardi.android.speeddial.SpeedDialView; import android.annotation.SuppressLint; diff --git a/app/src/main/java/com/amaze/filemanager/utils/NetworkUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/NetworkUtil.kt new file mode 100644 index 0000000000..8bf5b4b4d6 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/NetworkUtil.kt @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2014-2022 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.app.Service +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.wifi.WifiManager +import android.os.Build +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.net.InetAddress +import java.net.NetworkInterface +import java.net.UnknownHostException + +object NetworkUtil { + + private val log: Logger = LoggerFactory.getLogger(NetworkUtil::class.java) + + private fun getConnectivityManager(context: Context) = + context.applicationContext.getSystemService(Service.CONNECTIVITY_SERVICE) + as ConnectivityManager + + /** + * Is the device connected to local network, either Ethernet or Wifi? + */ + @JvmStatic + fun isConnectedToLocalNetwork(context: Context): Boolean { + val cm = getConnectivityManager(context) + var connected: Boolean + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + connected = cm.activeNetwork?.let { activeNetwork -> + cm.getNetworkCapabilities(activeNetwork)?.let { ni -> + ni.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) or + ni.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + } ?: false + } ?: false + } else { + connected = cm.activeNetworkInfo?.let { ni -> + ni.isConnected && ( + ni.type and ( + ConnectivityManager.TYPE_WIFI + or ConnectivityManager.TYPE_ETHERNET + ) != 0 + ) + } ?: false + } + + if (!connected) { + connected = runCatching { + NetworkInterface.getNetworkInterfaces().toList().find { netInterface -> + netInterface.displayName.startsWith("rndis") or + netInterface.displayName.startsWith("wlan") + } + }.getOrElse { null } != null + } + + return connected + } + + /** + * Is the device connected to Wifi? + */ + @JvmStatic + fun isConnectedToWifi(context: Context): Boolean { + val cm = getConnectivityManager(context) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + cm.activeNetwork?.let { + cm.getNetworkCapabilities(it)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + } ?: false + } else { + cm.activeNetworkInfo?.let { + it.isConnected && it.type == ConnectivityManager.TYPE_WIFI + } ?: false + } + } + + /** + * Determine device's IP address. + * + * Caveat: doesn't handle IPv6 addresses well. + */ + @JvmStatic + fun getLocalInetAddress(context: Context): InetAddress? { + if (!isConnectedToLocalNetwork(context)) { + return null + } + if (isConnectedToWifi(context)) { + val wm = context.applicationContext.getSystemService(Service.WIFI_SERVICE) + as WifiManager + val ipAddress = wm.connectionInfo.ipAddress + return if (ipAddress == 0) null else intToInet(ipAddress) + } + runCatching { + NetworkInterface.getNetworkInterfaces().iterator().forEach { netinterface -> + netinterface.inetAddresses.iterator().forEach { address -> + // this is the condition that sometimes gives problems + if (!address.isLoopbackAddress && + !address.isLinkLocalAddress + ) { + return address + } + } + } + }.onFailure { e -> + log.warn("failed to get local inet address", e) + } + return null + } + + /** + * Utility method to convert an IPv4 address in integer representation to [InetAddress]. + */ + @JvmStatic + fun intToInet(value: Int): InetAddress? { + val bytes = ByteArray(4) + for (i in 0..3) { + bytes[i] = byteOfInt(value, i) + } + return try { + InetAddress.getByAddress(bytes) + } catch (e: UnknownHostException) { + // This only happens if the byte array has a bad length + null + } + } + + private fun byteOfInt(value: Int, which: Int): Byte { + val shift = which * 8 + return (value shr shift).toByte() + } +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/SubnetScanner.java b/app/src/main/java/com/amaze/filemanager/utils/SubnetScanner.java deleted file mode 100644 index 3f6abfbbf7..0000000000 --- a/app/src/main/java/com/amaze/filemanager/utils/SubnetScanner.java +++ /dev/null @@ -1,252 +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.utils; - -/** Created by arpitkh996 on 16-01-2016. */ -import static com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX; - -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import android.content.Context; -import android.net.wifi.WifiManager; -import android.os.AsyncTask; -import android.text.format.Formatter; - -import jcifs.Address; -import jcifs.CIFSException; -import jcifs.NetbiosAddress; -import jcifs.context.SingletonContext; -import jcifs.smb.SmbFile; - -public class SubnetScanner extends AsyncTask { - - private static final String TAG = SubnetScanner.class.getSimpleName(); - private static final int RETRY_COUNT = 5; - private static boolean initialized = false; - - private Thread bdThread; - private final Object mLock; - private List mResults; - private ScanObserver observer; - private ExecutorService pool; - private List> tasks; - private Context context; - - public interface ScanObserver { - void computerFound(ComputerParcelable computer); - - void searchFinished(); - } - - class Task implements Callable { - String addr; - - Task(String str) { - this.addr = str; - } - - public ComputerParcelable call() { - try { - NetbiosAddress[] allByAddress = - SingletonContext.getInstance().getNameServiceClient().getNbtAllByAddress(this.addr); - if (allByAddress == null || allByAddress.length <= 0) { - return new ComputerParcelable(null, this.addr); - } - return new ComputerParcelable(allByAddress[0].getHostName(), this.addr); - } catch (UnknownHostException e) { - return new ComputerParcelable(null, this.addr); - } - } - } - - public static void init() { - Properties props = new Properties(); - props.setProperty("jcifs.resolveOrder", "BCAST"); - props.setProperty("jcifs.smb.client.responseTimeout", "30000"); - props.setProperty("jcifs.netbios.retryTimeout", "5000"); - props.setProperty("jcifs.netbios.cachePolicy", "-1"); - try { - SingletonContext.init(props); - initialized = true; - } catch (CIFSException e) { - android.util.Log.e(TAG, "Error initializing jcifs", e); - } - } - - public SubnetScanner(Context context) { - this.context = context; - mLock = new Object(); - tasks = new ArrayList<>(260); - pool = Executors.newFixedThreadPool(60); - mResults = new ArrayList<>(); - } - - @Override - protected Void doInBackground(Void... voids) { - - if (!initialized) init(); - - int ipAddress = - ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE)) - .getConnectionInfo() - .getIpAddress(); - if (ipAddress != 0) { - tryWithBroadcast(); - String formatIpAddress = Formatter.formatIpAddress(ipAddress); - String substring = formatIpAddress.substring(0, formatIpAddress.lastIndexOf(46) + 1); - if (!isCancelled()) { - for (ipAddress = 0; ipAddress < 100; ipAddress++) { - this.tasks.add(this.pool.submit(new Task(substring + ipAddress))); - this.tasks.add(this.pool.submit(new Task(substring + (ipAddress + 100)))); - if (ipAddress < 56) { - this.tasks.add(this.pool.submit(new Task(substring + (ipAddress + 200)))); - } - } - while (!this.tasks.isEmpty()) { - int size = this.tasks.size(); - int i = 0; - while (i < size) { - if (!isCancelled()) { - try { - ComputerParcelable computer = - (ComputerParcelable) ((Future) this.tasks.get(i)).get(1, TimeUnit.MILLISECONDS); - this.tasks.remove(i); - size--; - if (computer.getName() != null) { - publishProgress(computer); - } - ipAddress = size; - } catch (InterruptedException e) { - return null; - } catch (ExecutionException e2) { - ipAddress = size; - } catch (TimeoutException e3) { - ipAddress = size; - } - i++; - size = ipAddress; - } else { - return null; - } - } - } - try { - this.bdThread.join(); - } catch (InterruptedException e4) { - } - } else { - return null; - } - } - synchronized (this.mLock) { - if (this.observer != null) { - this.observer.searchFinished(); - } - } - - return null; - } - - private void tryWithBroadcast() { - this.bdThread = - new Thread() { - public void run() { - for (int i = 0; i < SubnetScanner.RETRY_COUNT; i++) { - try { - SmbFile smbFile = SmbUtil.create(SMB_URI_PREFIX); - smbFile.setConnectTimeout(5000); - SmbFile[] listFiles = smbFile.listFiles(); - for (SmbFile smbFile2 : listFiles) { - SmbFile[] listFiles2 = smbFile2.listFiles(); - for (SmbFile files : listFiles2) { - try { - String substring = files.getName().substring(0, files.getName().length() - 1); - Address byName = - SingletonContext.getInstance() - .getNameServiceClient() - .getByName(substring); - if (byName != null) { - publishProgress(new ComputerParcelable(substring, byName.getHostAddress())); - } - } catch (Throwable e) { - - } - } - } - } catch (Throwable e2) { - - } - } - } - }; - this.bdThread.start(); - } - - @Override - protected void onPreExecute() {} - - @Override - protected void onPostExecute(Void aVoid) { - this.pool.shutdown(); - } - - @Override - protected void onProgressUpdate(ComputerParcelable... computers) { - for (ComputerParcelable computer : computers) { - mResults.add(computer); - synchronized (this.mLock) { - if (this.observer != null) { - this.observer.computerFound(computer); - } - } - } - } - - public void setObserver(ScanObserver scanObserver) { - synchronized (this.mLock) { - this.observer = scanObserver; - } - } - - @Override - protected void onCancelled(Void aVoid) { - super.onCancelled(aVoid); - try { - this.pool.shutdownNow(); - } catch (Throwable th) { - - } - } - - public List getResults() { - return new ArrayList<>(this.mResults); - } -} diff --git a/app/src/main/java/com/amaze/filemanager/utils/UUIDv5.kt b/app/src/main/java/com/amaze/filemanager/utils/UUIDv5.kt new file mode 100644 index 0000000000..344482d034 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/UUIDv5.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2014-2022 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 java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.util.* + +/** + * UUIDv5 implementation, referenced from + * https://gist.github.com/icedraco/00118b4d3c91d96d8c58e837a448f1b8 + */ +object UUIDv5 { + + // Constants defined in RFC4122 https://www.ietf.org/rfc/rfc4122.txt + @JvmStatic + val DNS: UUID = UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + + @JvmStatic + val URL: UUID = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8") + + @JvmStatic + val OID: UUID = UUID.fromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8") + + @JvmStatic + val X500: UUID = UUID.fromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8") + + /** + * Generate an UUIDv5 UUID from given namespace UUID and name. + * + * [namespaceUUID] must be one of [DNS], [URL], [OID], [X500]. + */ + @JvmStatic + @Suppress("TooGenericExceptionThrown") + fun fromString(namespaceUUID: UUID, name: String): UUID { + val md: MessageDigest + try { + md = MessageDigest.getInstance("SHA-1") + } catch (ex: NoSuchAlgorithmException) { + throw Exception("SHA-1 not supported", ex) + } + + md.update(toBytes(namespaceUUID)) + md.update(name.toByteArray()) + val bytes = md.digest() + /* clear version; set to version 5 */ + bytes[6] = ((bytes[6].toInt() and 0x0F) or 0x50).toByte() + /* clear variant; set to IETF variant */ + bytes[8] = ((bytes[8].toInt() and 0x3F) or 0x80).toByte() + return fromBytes(bytes) + } + + private fun fromBytes(data: ByteArray): UUID { + // Based on the private UUID(bytes[]) constructor + assert(data.size >= 16) + var msb = 0L + var lsb = 0L + for (i in 0..7) + msb = msb shl 8 or (data[i].toLong() and 0xff) + for (i in 8..15) + lsb = lsb shl 8 or (data[i].toLong() and 0xff) + return UUID(msb, lsb) + } + + private fun toBytes(uuid: UUID): ByteArray { + // inverted logic of fromBytes() + val out = ByteArray(16) + val msb = uuid.mostSignificantBits + val lsb = uuid.leastSignificantBits + for (i in 0..7) + out[i] = (msb shr (7 - i) * 8 and 0xff).toByte() + for (i in 8..15) + out[i] = (lsb shr (15 - i) * 8 and 0xff).toByte() + return out + } +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt new file mode 100644 index 0000000000..277150c647 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2014-2022 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.smb + +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.utils.ComputerParcelable +import com.amaze.filemanager.utils.NetworkUtil +import com.stealthcopter.networktools.PortScan +import io.reactivex.Flowable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import java.net.Inet6Address +import java.net.InetAddress + +/** + * [SmbDeviceScannerObservable.DiscoverDeviceStrategy] to just loop through other addresses within + * same subnet (/24 netmask) and knock their SMB service ports for reachability. + * + * Will bypass [Inet6Address] device addresses. They may have much bigger neighourhood host count; + * also for devices using IPv6, they shall be covered by [WsddDiscoverDeviceStrategy] anyway. + * + * TODO: if we can get the gateway using __legit__ API, may swarm the network in broader netmasks + */ +class SameSubnetDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDeviceStrategy { + + private lateinit var worker: Disposable + + companion object { + private const val HOST_UP_TIMEOUT = 1000 + private const val PARALLELISM = 10 + private val TCP_PORTS = arrayListOf(139, 445) + } + + /** + * No need to cleanup resources + */ + override fun onCancel() { + if (!worker.isDisposed) { + worker.dispose() + } + } + + override fun discoverDevices(callback: (ComputerParcelable) -> Unit) { + val neighbourhoods = getNeighbourhoodHosts() + worker = Flowable.fromIterable(neighbourhoods) + .parallel(PARALLELISM) + .runOn(Schedulers.io()) + .map { addr -> + if (addr.isReachable(HOST_UP_TIMEOUT)) { + val portsReachable = listOf( + PortScan.onAddress(addr).setPorts(TCP_PORTS).setMethodTCP().doScan() + ).flatten() + if (portsReachable.isNotEmpty()) { + addr + } else { + false + } + } else { + false + } + }.filter { + it is InetAddress + }.doOnNext { addr -> + addr as InetAddress + callback.invoke(ComputerParcelable(addr.hostAddress, addr.hostName)) + }.sequential().subscribe() + } + + private fun getNeighbourhoodHosts(): List { + val deviceAddress = NetworkUtil.getLocalInetAddress(AppConfig.getInstance()) + return deviceAddress?.let { addr -> + if (addr is Inet6Address) { + // IPv6 neigbourhood hosts can be very big - that should use wsdd instead; hence + // empty list here + emptyList() + } else { + val networkPrefix: String = addr.hostAddress.substringBeforeLast('.') + (1..254).map { + InetAddress.getByName("$networkPrefix.$it") + } + } + } ?: emptyList() + } +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/smb/SmbDeviceScannerObservable.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/SmbDeviceScannerObservable.kt new file mode 100644 index 0000000000..14dcced9d8 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/SmbDeviceScannerObservable.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2014-2022 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.smb + +import com.amaze.filemanager.utils.ComputerParcelable +import com.amaze.filemanager.utils.smb.SmbDeviceScannerObservable.DiscoverDeviceStrategy +import io.reactivex.Observable +import io.reactivex.Observer +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import java.net.InetAddress + +/** + * Observable to discover reachable SMB nodes on the network. + * + * Uses a series of [DiscoverDeviceStrategy] instances to discover nodes. + */ +class SmbDeviceScannerObservable : Observable() { + + /** + * Device discovery strategy interface. + */ + interface DiscoverDeviceStrategy { + /** + * Implement this method to return list of [InetAddress] which has SMB service running. + */ + fun discoverDevices(callback: (ComputerParcelable) -> Unit) + + /** + * Implement this method to cleanup resources + */ + fun onCancel() + } + + private var discoverDeviceStrategies: Array = + arrayOf( + WsddDiscoverDeviceStrategy(), + SameSubnetDiscoverDeviceStrategy() + ) + + private lateinit var observer: Observer + + private lateinit var disposable: Disposable + + /** + * Stop discovering hosts. Notify containing strategies to stop, then stop the created + * [Observer] obtained at [subscribeActual]. + */ + fun stop() { + if (!disposable.isDisposed) { + disposable.dispose() + } + observer.onComplete() + } + + /** + * Call all strategies one by one to discover nodes. + * + * Given observer must be able to drop duplicated entries (which ComputerParcelable already + * has implemented equals() and hashCode()). + */ + override fun subscribeActual(observer: Observer) { + this.observer = observer + this.disposable = merge( + discoverDeviceStrategies.map { strategy -> + fromCallable { + strategy.discoverDevices { addr -> + observer.onNext(ComputerParcelable(addr.addr, addr.name)) + } + }.subscribeOn(Schedulers.io()) + } + ).observeOn(Schedulers.computation()).doOnComplete { + discoverDeviceStrategies.forEach { strategy -> + strategy.onCancel() + } + }.subscribe() + } +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/SmbUtil.kt similarity index 98% rename from app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt rename to app/src/main/java/com/amaze/filemanager/utils/smb/SmbUtil.kt index 35ae7bd5b9..eca1603f92 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/SmbUtil.kt @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.amaze.filemanager.utils +package com.amaze.filemanager.utils.smb import android.content.Context import android.net.Uri @@ -30,6 +30,8 @@ import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.AT import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.COLON import com.amaze.filemanager.filesystem.smb.CifsContexts.createWithDisableIpcSigningCheck +import com.amaze.filemanager.utils.PasswordUtil +import com.amaze.filemanager.utils.urlDecoded import io.reactivex.Single import io.reactivex.schedulers.Schedulers import jcifs.smb.NtlmPasswordAuthenticator diff --git a/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt new file mode 100644 index 0000000000..33e1813c69 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2014-2022 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.smb + +import androidx.annotation.VisibleForTesting +import com.amaze.filemanager.R +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.utils.ComputerParcelable +import com.amaze.filemanager.utils.NetworkUtil +import com.android.volley.Response.ErrorListener +import com.android.volley.VolleyError +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import org.xmlpull.v1.XmlPullParserFactory +import java.io.StringReader +import java.net.DatagramPacket +import java.net.InetAddress +import java.net.MulticastSocket +import java.util.* + +/** + * [SmbDeviceScannerObservable.DiscoverDeviceStrategy] implementation to discover SMB devices using + * [Web service discovery](https://en.wikipedia.org/wiki/WS-Discovery), which is used by SMBv2 or + * above. + * + * Discovery method goes this way: + * 1. send a SOAP request to multicast address 239.255.255.250 port 3702 over UDP + * 2. for each reply as SOAP XML too, extract their URN and record the address the packets are from + * 3. if the reply indicates sender is a computer, send a HTTP POST to the address recorded in 2, port 5357 + * 4. verify result and send [ComputerParcelable] in callback + * + * Implementation is after reference: https://fitzcarraldoblog.wordpress.com/2020/07/08/a-linux-command-line-utility-to-discover-and-list-wsd-enabled-computers-and-printers-on-a-home-network/ + * (Python though). + * + * Original implementation calls for UUIDv5 which will use hash value of the device's MAC address; + * this implementation is not using, since MAC address poses privacy concern, and newer Androids are + * making difficult to fetch MAC addresses anyway. + * + * Manually setting [multicastSocketFactory] Allows customized method to be specified for creating [MulticastSocket] + * for convenience of testing. + * + * @author TranceLove + */ +class WsddDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDeviceStrategy { + + private val multicastRequestTemplate = AppConfig.getInstance() + .resources.openRawResource(R.raw.wsdd_discovery) + .reader(Charsets.UTF_8).readText() + + private val wsdRequestTemplate = AppConfig.getInstance() + .resources.openRawResource(R.raw.wsd_request) + .reader(Charsets.UTF_8).readText() + + private val wsdRequestHeaders = mutableMapOf( + Pair("Accept-Encoding", "Identity"), + Pair("Content-Type", "application/soap+xml"), + Pair("Connection", "Close"), + Pair("User-Agent", "wsd") + ) + + var multicastSocketFactory: () -> MulticastSocket = DEFAULT_MULTICAST_SOCKET_FACTORY + @VisibleForTesting + get + + @VisibleForTesting + set + + private val queue = Volley.newRequestQueue(AppConfig.getInstance()) + + private var cancelled = false + + init { + queue.start() + } + + override fun discoverDevices(callback: (ComputerParcelable) -> Unit) { + multicastForDevice { addr -> + callback.invoke(addr) + } + } + + @Suppress("LabeledExpression") + private fun multicastForDevice(callback: (ComputerParcelable) -> Unit) { + NetworkUtil.getLocalInetAddress(AppConfig.getInstance())?.let { addr -> + val multicastAddressV4 = InetAddress.getByName(BROADCAST_IPV4) + val multicastAddressV6 = InetAddress.getByName(BROADCAST_IPV6_LINK_LOCAL) + + while (!cancelled) { + val socket: MulticastSocket = multicastSocketFactory.invoke() + socket.timeToLive = 1 + socket.soTimeout = SOCKET_RECEIVE_TIMEOUT + socket.reuseAddress = true + socket.joinGroup(multicastAddressV4) + socket.joinGroup(multicastAddressV6) + + // Specification said UUIDv5 which is device dependent. But random-based UUID should + // also work here + val tempDeviceUuid = UUID.randomUUID() + val request = multicastRequestTemplate + .replace("##MY_UUID##", tempDeviceUuid.toString()) + .toByteArray(Charsets.UTF_8) + + val requestPacket = DatagramPacket( + request, + request.size, + multicastAddressV4, + UDP_PORT + ) + socket.send(requestPacket) + + runCatching { + while (!socket.isClosed) { + val buffer = ByteArray(4096) + val replyPacket = DatagramPacket(buffer, buffer.size) + socket.receive(replyPacket) + if (replyPacket.data.isNotEmpty() && replyPacket.address != null) { + val sentFromAddress = replyPacket.address + queryWithResponseAsNecessary( + sentFromAddress, + tempDeviceUuid.toString(), + replyPacket.data, + callback + ) + } + } + }.onFailure { + if (log.isWarnEnabled) log.warn("Error receiving reply", it) + socket.close() + } + } + } + } + + private fun queryWithResponseAsNecessary( + sourceAddress: InetAddress, + tempDeviceId: String, + response: ByteArray, + callback: (ComputerParcelable) -> Unit + ) { + val values = parseXmlForResponse(response, arrayOf(WSD_TYPES, WSA_ADDRESS)) + val type = values[WSD_TYPES] + val urn = values[WSA_ADDRESS] + + if (true == type?.isNotEmpty() && true == urn?.isNotEmpty()) { + queryEndpointForResponse(type, sourceAddress, urn, tempDeviceId, callback) + } + } + + private fun queryEndpointForResponse( + type: String, + sourceAddress: InetAddress, + urn: String, + tempDeviceId: String, + callback: (ComputerParcelable) -> Unit + ) { + if (type.endsWith(PUB_COMPUTER)) { + val messageId = UUID.randomUUID().toString() + + val endpoint = urn.substringAfter(URN_UUID) + val dest = + "http://${sourceAddress.hostAddress}:$TCP_PORT/$endpoint" + queue.add( + object : StringRequest( + Method.POST, + dest, + { resp -> + if (log.isTraceEnabled) log.trace("Response: $resp") + val values = parseXmlForResponse( + resp, + arrayOf(WSDP_TYPES, WSA_ADDRESS, PUB_COMPUTER) + ) + if (PUB_COMPUTER == values[WSDP_TYPES] && urn == values[WSA_ADDRESS]) { + if (true == values[PUB_COMPUTER]?.isNotEmpty()) { + val computerName: String = values[PUB_COMPUTER].let { + if (it!!.contains('/')) { + it.substringBefore("/") + } else { + it + } + } + callback( + ComputerParcelable(sourceAddress.hostAddress, computerName) + ) + } + } + }, + object : ErrorListener { + override fun onErrorResponse(error: VolleyError?) { + log.error("Error querying endpoint", error) + } + } + ) { + override fun getBody(): ByteArray { + return wsdRequestTemplate + .replace("##MESSAGE_ID##", "$URN_UUID$messageId") + .replace("##DEST_UUID##", urn) + .replace("##MY_UUID##", "$URN_UUID$tempDeviceId") + .toByteArray(Charsets.UTF_8) + } + override fun getHeaders(): MutableMap = wsdRequestHeaders + } + ) + } + } + + override fun onCancel() { + cancelled = true + queue.stop() + } + + private fun parseXmlForResponse(xml: ByteArray, tags: Array) = + parseXmlForResponse(xml.toString(Charsets.UTF_8), tags) + + private fun parseXmlForResponse(xml: String, tags: Array): Map { + if (xml.isEmpty()) { + return emptyMap() + } else { + val xmlParser = XmlPullParserFactory.newInstance().also { + it.isNamespaceAware = false + it.isValidating = false + }.newPullParser().also { + it.setInput(StringReader(xml)) + } + val retval = WeakHashMap() + var currentTag: String = "" + var currentValue: String = "" + var event = xmlParser.eventType + try { + while (event != XmlPullParser.END_DOCUMENT) { + if (event == XmlPullParser.START_TAG) { + currentTag = xmlParser.name + } else if (event == XmlPullParser.TEXT) { + currentValue = xmlParser.text + } else if (event == XmlPullParser.END_TAG) { + if (tags.contains(currentTag)) { + retval[currentTag] = currentValue + currentTag = "" + currentValue = "" + } + } + event = xmlParser.next() + } + } catch (parseError: XmlPullParserException) { + log.warn("Error parsing XML", parseError) + // Combination of parsed result is required, hence it's all or nothing situation - + // if one error found, whole XML will not be valid. Clear for "no result" answer + retval.clear() + } + return retval + } + } + + companion object { + private const val BROADCAST_IPV4 = "239.255.255.250" + private const val BROADCAST_IPV6_LINK_LOCAL = "[FF02::C]" + private const val UDP_PORT = 3702 + private const val TCP_PORT = 5357 + private const val SOCKET_RECEIVE_TIMEOUT = 60000 // 1 minute receive timeout + + private const val URN_UUID = "urn:uuid:" + private const val WSA_ADDRESS = "wsa:Address" + private const val WSD_TYPES = "wsd:Types" + private const val WSDP_TYPES = "wsdp:Types" + private const val PUB_COMPUTER = "pub:Computer" + + private val log: Logger = LoggerFactory.getLogger(WsddDiscoverDeviceStrategy::class.java) + + private val DEFAULT_MULTICAST_SOCKET_FACTORY: () -> MulticastSocket = { + MulticastSocket() + } + } +} diff --git a/app/src/main/res/raw/wsd_request.txt b/app/src/main/res/raw/wsd_request.txt new file mode 100644 index 0000000000..e7ae2d6f85 --- /dev/null +++ b/app/src/main/res/raw/wsd_request.txt @@ -0,0 +1,22 @@ + + + + ##DEST_UUID## + http://schemas.xmlsoap.org/ws/2004/09/transfer/Get + ##MESSAGE_ID## + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + ##MY_UUID## + + + + \ No newline at end of file diff --git a/app/src/main/res/raw/wsdd_discovery.txt b/app/src/main/res/raw/wsdd_discovery.txt new file mode 100644 index 0000000000..aa39fe0676 --- /dev/null +++ b/app/src/main/res/raw/wsdd_discovery.txt @@ -0,0 +1 @@ +urn:schemas-xmlsoap-org:ws:2005:04:discoveryhttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probeurn:uuid:##MY_UUID##wsdp:Device \ No newline at end of file diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/SmbDeleteTaskTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/SmbDeleteTaskTest.kt index 9bd9b32b37..d440d38285 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/SmbDeleteTaskTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/SmbDeleteTaskTest.kt @@ -22,7 +22,7 @@ package com.amaze.filemanager.asynchronous.asynctasks import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.shadows.ShadowSmbUtil.Companion.PATH_CANNOT_DELETE_FILE -import com.amaze.filemanager.utils.SmbUtil +import com.amaze.filemanager.utils.smb.SmbUtil import org.junit.Test class SmbDeleteTaskTest : AbstractDeleteTaskTestBase() { diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt index 85fc4c54a0..e790bfca0f 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt @@ -39,7 +39,7 @@ import com.amaze.filemanager.database.UtilitiesDatabase.Companion.TABLE_SMB import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowPasswordUtil import com.amaze.filemanager.utils.PasswordUtil -import com.amaze.filemanager.utils.SmbUtil +import com.amaze.filemanager.utils.smb.SmbUtil import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt index 9a208729fa..842c7506f8 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt @@ -35,7 +35,7 @@ import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.database.models.OperationData import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowPasswordUtil -import com.amaze.filemanager.utils.SmbUtil +import com.amaze.filemanager.utils.smb.SmbUtil import io.reactivex.android.plugins.RxAndroidPlugins import io.reactivex.plugins.RxJavaPlugins import io.reactivex.schedulers.Schedulers diff --git a/app/src/test/java/com/amaze/filemanager/utils/ComputerParcelableTest.java b/app/src/test/java/com/amaze/filemanager/utils/ComputerParcelableTest.java index 1624c290a0..07a1abb70b 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/ComputerParcelableTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/ComputerParcelableTest.java @@ -54,15 +54,15 @@ public void testObjectNotEqualsName() { /** * Purpose: when computerParcelable's address and object's address are not the same, confirm that - * the two are different. Input: computerParcelable.equals(object) only ComputerParcelable.name == - * Object.name Expected: result is false + * the two are different. Input: computerParcelable.equals(object) only ComputerParcelable.addr == + * Object.name Expected: result is true */ @Test public void testObjectNotEqualsAddr() { ComputerParcelable computerParcelable = new ComputerParcelable("com1", "1"); Object object = new ComputerParcelable("com1", "2"); - assertFalse(computerParcelable.equals(object)); + assertTrue(computerParcelable.equals(object)); } /** diff --git a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt index 697fde2f18..352eec1d06 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt @@ -29,10 +29,10 @@ import com.amaze.filemanager.fileoperations.filesystem.DOESNT_EXIST import com.amaze.filemanager.fileoperations.filesystem.WRITABLE_ON_REMOTE import com.amaze.filemanager.shadows.ShadowSmbUtil import com.amaze.filemanager.test.ShadowPasswordUtil -import com.amaze.filemanager.utils.SmbUtil.checkFolder -import com.amaze.filemanager.utils.SmbUtil.createFrom -import com.amaze.filemanager.utils.SmbUtil.getSmbDecryptedPath -import com.amaze.filemanager.utils.SmbUtil.getSmbEncryptedPath +import com.amaze.filemanager.utils.smb.SmbUtil.checkFolder +import com.amaze.filemanager.utils.smb.SmbUtil.createFrom +import com.amaze.filemanager.utils.smb.SmbUtil.getSmbDecryptedPath +import com.amaze.filemanager.utils.smb.SmbUtil.getSmbEncryptedPath import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue diff --git a/app/src/test/java/com/amaze/filemanager/utils/UUIDv5Test.kt b/app/src/test/java/com/amaze/filemanager/utils/UUIDv5Test.kt new file mode 100644 index 0000000000..6f4261978f --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/utils/UUIDv5Test.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014-2022 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 org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Test for [UUIDv5]. + */ +class UUIDv5Test { + + /** + * Test UUID generation. Value is based on SHA-1 hash, so it can be expected. + * + * Test case taken (again) from + * https://gist.github.com/icedraco/00118b4d3c91d96d8c58e837a448f1b8 + */ + @Test + fun testGenerateUUID() { + val url = "http://www.whatever.com/test/" + val uuid = UUIDv5.fromString(UUIDv5.URL, url) + assertEquals("1730930d-a36a-5efd-aa3f-561a164f87a4", uuid.toString()) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/utils/smb/AbstractSubnetDiscoverDevicesStrategyTests.kt b/app/src/test/java/com/amaze/filemanager/utils/smb/AbstractSubnetDiscoverDevicesStrategyTests.kt new file mode 100644 index 0000000000..3b44ccdc1e --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/utils/smb/AbstractSubnetDiscoverDevicesStrategyTests.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014-2022 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.smb + +import android.os.Build.VERSION_CODES +import android.os.Build.VERSION_CODES.KITKAT +import android.os.Build.VERSION_CODES.P +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.utils.NetworkUtil +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import org.junit.After +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.net.InetAddress + +/** + * Base class for [SmbDeviceScannerObservable.DiscoverDeviceStrategy] tests. + */ +@RunWith(AndroidJUnit4::class) +@Config(sdk = [KITKAT, P, VERSION_CODES.R]) +abstract class AbstractSubnetDiscoverDevicesStrategyTests { + + /** + * Post test cleanup. + */ + @After + open fun tearDown() { + unmockkStatic(NetworkUtil::class) + } + + protected fun deviceOffline() { + mockkStatic(NetworkUtil::class) + every { NetworkUtil.isConnectedToWifi(any()) } returns false + every { NetworkUtil.isConnectedToLocalNetwork(any()) } returns false + every { NetworkUtil.getLocalInetAddress(any()) } returns null + } + + protected fun deviceOnline() { + mockkStatic(NetworkUtil::class) + every { NetworkUtil.isConnectedToWifi(any()) } returns true + every { NetworkUtil.isConnectedToLocalNetwork(any()) } returns true + every { NetworkUtil.getLocalInetAddress(any()) } returns mockk().also { + every { it.hostName } returns "192.168.233.240" + } + } + + protected fun mockInetAddress(hostName: String, hostAddress: String): InetAddress { + val upHost = mockk() + every { upHost.hostName } returns hostName + every { upHost.hostAddress } returns hostAddress + every { InetAddress.getByName(hostAddress) } returns upHost + return upHost + } +} diff --git a/app/src/test/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDevicesStrategyTest.kt b/app/src/test/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDevicesStrategyTest.kt new file mode 100644 index 0000000000..1cd428f1f6 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDevicesStrategyTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014-2022 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.smb + +import com.amaze.filemanager.utils.ComputerParcelable +import org.junit.Assert.assertEquals +import org.junit.Test +import org.slf4j.LoggerFactory +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class SameSubnetDiscoverDevicesStrategyTest : AbstractSubnetDiscoverDevicesStrategyTests() { + + companion object { + @JvmStatic + private val logger = LoggerFactory.getLogger( + SameSubnetDiscoverDevicesStrategyTest::class.java + ) + } + + /** + * Test if device is not connected to network. + */ + @Test + fun testDiscoverIfNotConnected() { + deviceOffline() + val latch = CountDownLatch(1) + val result = ArrayList() + SameSubnetDiscoverDeviceStrategy().discoverDevices { + result.add(it) + latch.countDown() + } + try { + latch.await(1, TimeUnit.SECONDS) + } catch (_: Throwable) { + latch.countDown() + } + assertEquals(0, result.size) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/utils/smb/WsddSubnetDiscoverDevicesStrategyTest.kt b/app/src/test/java/com/amaze/filemanager/utils/smb/WsddSubnetDiscoverDevicesStrategyTest.kt new file mode 100644 index 0000000000..0212756d3b --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/utils/smb/WsddSubnetDiscoverDevicesStrategyTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2014-2022 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.smb + +import com.amaze.filemanager.test.randomBytes +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.robolectric.util.ReflectionHelpers +import java.util.* +import kotlin.text.Charsets.UTF_8 + +/** + * Test [WsddDiscoverDeviceStrategy]. + */ +@Suppress("StringLiteralDuplication") +class WsddSubnetDiscoverDevicesStrategyTest : AbstractSubnetDiscoverDevicesStrategyTests() { + + private val multicastResponseTemplate = javaClass.classLoader!! + .getResourceAsStream("wsdd/multicast-response.txt") + .reader(UTF_8).readText() + + private val wsdResponseTemplate = javaClass.classLoader!! + .getResourceAsStream("wsdd/wsd-response.txt") + .reader(UTF_8).readText() + + private val parseXmlForResponse: + (WsddDiscoverDeviceStrategy, Any, Array) -> Map = + { instance, xml, tags -> + require((xml is ByteArray) or (xml is String)) + ReflectionHelpers.callInstanceMethod( + WsddDiscoverDeviceStrategy::class.java, + instance, + "parseXmlForResponse", + ReflectionHelpers.ClassParameter(xml.javaClass, xml), + ReflectionHelpers.ClassParameter(Array::class.java, tags) + ) + } + + private lateinit var wsdMulticastResponseMessageId: String + private lateinit var deviceId: String + + /** + * Test for normal parsing of multicast response + */ + @Test + fun testParseMulticastResponse() { + val instance = WsddDiscoverDeviceStrategy() + val result = parseXmlForResponse.invoke( + instance, + createMulticastResponse(), + arrayOf("wsd:Types", "wsa:Address") + ) + assertTrue(result.isNotEmpty()) + assertTrue(result.containsKey("wsd:Types")) + assertTrue(result.containsKey("wsa:Address")) + assertTrue(true == result["wsd:Types"]?.isNotBlank()) + assertTrue(true == result["wsa:Address"]?.isNotBlank()) + } + + /** + * Test parsing invalid XML and/or invalid/nonexistent tags in XML. + */ + @Test + fun testParseInvalidMulticastResponse() { + val instance = WsddDiscoverDeviceStrategy() + assertTrue(parseXmlForResponse.invoke(instance, "", emptyArray()).isEmpty()) + assertTrue(parseXmlForResponse.invoke(instance, "foobar", emptyArray()).isEmpty()) + assertTrue(parseXmlForResponse.invoke(instance, "", emptyArray()).isEmpty()) + assertTrue( + parseXmlForResponse.invoke( + instance, + ByteArray(0), + emptyArray() + ).isEmpty() + ) + assertTrue( + parseXmlForResponse.invoke( + instance, + "foobar".toByteArray(), + emptyArray() + ).isEmpty() + ) + assertTrue( + parseXmlForResponse.invoke( + instance, + randomBytes(), + emptyArray() + ).isEmpty() + ) + } + + /** + * Test parsing of valid XML but with non-matching tags in XML. + */ + @Test + fun testParseNonMatchingMulticastResponseParams() { + val instance = WsddDiscoverDeviceStrategy() + assertEquals( + 0, + parseXmlForResponse.invoke( + instance, + "", + arrayOf("foobar") + ).size + ) + assertEquals( + 0, + parseXmlForResponse.invoke( + instance, + "", + arrayOf("test") + ).size + ) + } + + private fun createMulticastResponse(): String { + return multicastResponseTemplate.replace( + "##DEVICE_UUID##", + UUID.randomUUID().toString() + ).replace( + "##MESSAGE_ID##", + UUID.randomUUID().toString() + ).replace( + "##SRC_MESSAGE_ID##", + UUID.randomUUID().toString() + ) + } + + private fun generateWsdResponse(deviceName: String, workgroupName: String = "WORKGROUP") = + wsdResponseTemplate + .replace("##THIS_DEVICE_ID##", deviceId) + .replace("##DEVICE_NAME##", deviceName) + .replace("##WORKGROUP_NAME##", workgroupName) + .replace("##PREV_MESSAGE_ID##", wsdMulticastResponseMessageId) + .replace("##THIS_MESSAGE_ID##", UUID.randomUUID().toString()) + .toByteArray(UTF_8) +} diff --git a/app/src/test/resources/wsdd/multicast-response.txt b/app/src/test/resources/wsdd/multicast-response.txt new file mode 100644 index 0000000000..5a8af6aa9c --- /dev/null +++ b/app/src/test/resources/wsdd/multicast-response.txt @@ -0,0 +1,28 @@ + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches + urn:uuid:##MESSAGE_ID## + urn:uuid:##SRC_MESSAGE_ID## + + + + + + + urn:uuid:##DEVICE_UUID## + + wsdp:Device pub:Computer + 1 + + + + \ No newline at end of file diff --git a/app/src/test/resources/wsdd/wsd-response.txt b/app/src/test/resources/wsdd/wsd-response.txt new file mode 100644 index 0000000000..7eb119a43f --- /dev/null +++ b/app/src/test/resources/wsdd/wsd-response.txt @@ -0,0 +1,46 @@ + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse + urn:uuid:##THIS_MESSAGE_ID## + urn:uuid:##PREV_MESSAGE_ID## + + + + + + WSD Device ##DEVICE_NAME## + 1.0 + 1 + + + + + wsdd + wsdd + Computers + + + + + + + urn:uuid:##THIS_DEVICE_ID## + + pub:Computer + urn:uuid:##THIS_DEVICE_ID## + ##DEVICE_NAME##/Workgroup:##WORKGROUP_NAME## + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7197d6553f..3fd0ef3566 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,8 @@ buildscript { commonsNetVersion = "3.8.0" ftpserverVersion = "1.1.1" jsoupVersion = "1.13.1" + rxAndroidVersion = "2.1.1" + rxJavaVersion = "2.2.9" } repositories { google() @@ -76,7 +78,7 @@ allprojects { spotless { java { licenseHeaderFile 'spotless.license-java' - target 'app/src/**/*.java', 'commons_compress_7z/src/**/*.java', 'file_operations/src/**/*.java' + target 'app/src/**/*.java', 'commons_compress_7z/src/**/*.java', 'file_operations/src/**/*.java', 'portscanner/src/**/*.java' googleJavaFormat('1.15.0') removeUnusedImports() // removes any unused imports importOrder 'java', 'javax', 'org', 'com', 'android', 'androidx', '' diff --git a/portscanner/.gitignore b/portscanner/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/portscanner/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/portscanner/build.gradle b/portscanner/build.gradle new file mode 100644 index 0000000000..355d17f1c3 --- /dev/null +++ b/portscanner/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' + +android { + namespace 'com.stealthcopter.networktools' + compileSdk 32 + + defaultConfig { + minSdk 14 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" + // Because RxAndroid releases are few and far between, it is recommended you also + // explicitly depend on RxJava's latest version for bug fixes and new features. + // (see https://github.com/ReactiveX/RxJava/releases for latest 3.x.x version) + implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" + testImplementation 'junit:junit:4.13.2' +} \ No newline at end of file diff --git a/portscanner/consumer-rules.pro b/portscanner/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portscanner/proguard-rules.pro b/portscanner/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/portscanner/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/portscanner/src/main/AndroidManifest.xml b/portscanner/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..100ab263c3 --- /dev/null +++ b/portscanner/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/portscanner/src/main/java/com/stealthcopter/networktools/IPTools.kt b/portscanner/src/main/java/com/stealthcopter/networktools/IPTools.kt new file mode 100644 index 0000000000..59fac0768e --- /dev/null +++ b/portscanner/src/main/java/com/stealthcopter/networktools/IPTools.kt @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.stealthcopter.networktools + +import java.net.Inet4Address +import java.net.InetAddress +import java.net.NetworkInterface +import java.net.SocketException +import java.util.regex.Pattern + +/** + * Created by mat on 14/12/15. + */ +object IPTools { + /* + * Ip matching patterns from + * https://examples.javacodegeeks.com/core-java/util/regex/regular-expressions-for-ip-v4-and-ip-v6-addresses/ + * note that these patterns will match most but not all valid ips + */ + private val IPV4_PATTERN = Pattern.compile( + "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$" + ) + private val IPV6_STD_PATTERN = Pattern.compile( + "^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$" + ) + private val IPV6_HEX_COMPRESSED_PATTERN = Pattern.compile( + "^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$" + ) + + /** + * Answers if given string is valid IPv4 address. + */ + @JvmStatic + fun isIPv4Address(address: String?): Boolean { + return address != null && IPV4_PATTERN.matcher(address).matches() + } + + /** + * Answers if given string is valid IPv6 address in long form. + */ + @JvmStatic + fun isIPv6StdAddress(address: String?): Boolean { + return address != null && IPV6_STD_PATTERN.matcher(address).matches() + } + + /** + * Answers if given string is valid IPv6 address in hex compressed form. + */ + @JvmStatic + fun isIPv6HexCompressedAddress(address: String?): Boolean { + return address != null && IPV6_HEX_COMPRESSED_PATTERN.matcher(address).matches() + } + + /** + * Answers if given string is a valid IPv6 address. + */ + @JvmStatic + fun isIPv6Address(address: String?): Boolean { + return address != null && (isIPv6StdAddress(address) || isIPv6HexCompressedAddress(address)) + } + + /* + * @return The first local IPv4 address, or null + */ + @JvmStatic + val localIPv4Address: InetAddress? + get() { + val localAddresses = localIPv4Addresses + return if (localAddresses.isNotEmpty()) localAddresses[0] else null + } + + /* + * Return The list of all IPv4 addresses found + */ + private val localIPv4Addresses: List + get() = runCatching { + NetworkInterface.getNetworkInterfaces().toList().flatMap { iface -> + iface.inetAddresses.asSequence().filter { addr -> + addr is Inet4Address && !addr.isLoopbackAddress() + } + } + }.getOrDefault(emptyList()) + + /** + * Check if the provided ip address refers to the localhost + * + * https://stackoverflow.com/a/2406819/315998 + * + * @param addr - address to check + * @return - true if ip address is self + */ + @JvmStatic + fun isIpAddressLocalhost(addr: InetAddress?): Boolean { + return addr?.run { + // Check if the address is a valid special local or loop back + if (addr.isAnyLocalAddress || addr.isLoopbackAddress) true else try { + NetworkInterface.getByInetAddress(addr) != null + } catch (e: SocketException) { + false + } + } ?: false + } + + /** + * Check if the provided ip address refers to the localhost + * + * https://stackoverflow.com/a/2406819/315998 + * + * @param addr - address to check + * @return - true if ip address is self + */ + @JvmStatic + fun isIpAddressLocalNetwork(addr: InetAddress?): Boolean = + addr != null && addr.isSiteLocalAddress +} diff --git a/portscanner/src/main/java/com/stealthcopter/networktools/PortScan.kt b/portscanner/src/main/java/com/stealthcopter/networktools/PortScan.kt new file mode 100644 index 0000000000..0f9f9812ef --- /dev/null +++ b/portscanner/src/main/java/com/stealthcopter/networktools/PortScan.kt @@ -0,0 +1,315 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.stealthcopter.networktools + +import com.stealthcopter.networktools.IPTools.isIpAddressLocalNetwork +import com.stealthcopter.networktools.IPTools.isIpAddressLocalhost +import com.stealthcopter.networktools.portscanning.PortScanTCP +import com.stealthcopter.networktools.portscanning.PortScanUDP +import io.reactivex.Flowable +import io.reactivex.schedulers.Schedulers +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.* + +/** Created by mat on 14/12/15. */ +class PortScan // This class is not to be instantiated +private constructor() { + + private var method = METHOD_TCP + private var noThreads = 50 + private var address: InetAddress? = null + private var timeOutMillis = 1000 + private var cancelled = false + private var ports: MutableList = ArrayList() + private val openPortsFound: MutableList = ArrayList() + private var portListener: PortListener? = null + private lateinit var runningFlowable: Flowable + + interface PortListener { + /** + * Callback function for port scan result + */ + fun onResult(portNo: Int, open: Boolean) + + /** + * Callback function for receiving the list of opened ports + */ + fun onFinished(openPorts: List?) + } + + /** + * Sets the timeout for each port scanned + * + * If you raise the timeout you may want to consider increasing the thread count [ ][.setNoThreads] to compensate. We can afford to have quite a high thread count as most of + * the time the thread is just sitting idle and waiting for the socket to timeout. + * + * @param timeOutMillis - the timeout for each ping in milliseconds Recommendations: Local host: + * 20 - 500 ms - can be very fast as request doesn't need to go over network Local network 500 + * - 2500 ms Remote Scan 2500+ ms + * @return this object to allow chaining + */ + fun setTimeOutMillis(timeOutMillis: Int): PortScan { + require(timeOutMillis >= 0) { "Timeout cannot be less than 0" } + this.timeOutMillis = timeOutMillis + return this + } + + /** + * Scan the ports to scan + * + * @param port - the port to scan + * @return this object to allow chaining + */ + fun setPort(port: Int): PortScan { + ports.clear() + validatePort(port) + ports.add(port) + return this + } + + /** + * Scan the ports to scan + * + * @param ports - the ports to scan + * @return this object to allow chaining + */ + fun setPorts(ports: MutableList): PortScan { + // Check all ports are valid + for (port in ports) { + validatePort(port) + } + this.ports = ports + return this + } + + /** + * Scan the ports to scan + * + * @param portString - the ports to scan (comma separated, hyphen denotes a range). For example: + * "21-23,25,45,53,80" + * @return this object to allow chaining + */ + fun setPorts(portString: String): PortScan { + var portString = portString + ports.clear() + val ports: MutableList = ArrayList() + portString = portString.substring(portString.indexOf(":") + 1, portString.length) + for (x in portString.split(",").toTypedArray()) { + if (x.contains("-")) { + val start = x.split("-").toTypedArray()[0].toInt() + val end = x.split("-").toTypedArray()[1].toInt() + validatePort(start) + validatePort(end) + require(end > start) { "Start port cannot be greater than or equal to the end port" } + for (j in start..end) { + ports.add(j) + } + } else { + val start = x.toInt() + validatePort(start) + ports.add(start) + } + } + this.ports = ports + return this + } + + /** + * Checks and throws exception if port is not valid + * + * @param port - the port to validate + */ + private fun validatePort(port: Int) { + require(port >= 1) { "Start port cannot be less than 1" } + require(port <= 65535) { "Start cannot be greater than 65535" } + } + + private fun setAddress(address: InetAddress) { + this.address = address + } + + private fun setDefaultThreadsAndTimeouts() { + // Try and work out automatically what kind of host we are scanning + // local host (this device) / local network / remote + if (isIpAddressLocalhost(address)) { + // If we are scanning a the localhost set the timeout to be very short so we get faster + // results + // This will be overridden if user calls setTimeoutMillis manually. + timeOutMillis = TIMEOUT_LOCALHOST + noThreads = DEFAULT_THREADS_LOCALHOST + } else if (isIpAddressLocalNetwork(address)) { + // Assume local network (not infallible) + timeOutMillis = TIMEOUT_LOCALNETWORK + noThreads = DEFAULT_THREADS_LOCALNETWORK + } else { + // Assume remote network timeouts + timeOutMillis = TIMEOUT_REMOTE + noThreads = DEFAULT_THREADS_REMOTE + } + } + + /** + * @param noThreads set the number of threads to work with, note we default to a large number as + * these requests are network heavy not cpu heavy. + * @return self + * @throws IllegalArgumentException - if no threads is less than 1 + */ + @Throws(IllegalArgumentException::class) + fun setNoThreads(noThreads: Int): PortScan { + require(noThreads >= 1) { "Cannot have less than 1 thread" } + this.noThreads = noThreads + return this + } + + /** + * Set scan method, either TCP or UDP + * + * @param method - the transport method to use to scan, either PortScan.METHOD_UDP or + * PortScan.METHOD_TCP + * @return this object to allow chaining + * @throws IllegalArgumentException - if invalid method + */ + private fun setMethod(method: Int): PortScan { + when (method) { + METHOD_UDP, METHOD_TCP -> this.method = method + else -> throw IllegalArgumentException("Invalid method type $method") + } + return this + } + + /** + * Set scan method to UDP + * + * @return this object to allow chaining + */ + fun setMethodUDP(): PortScan { + setMethod(METHOD_UDP) + return this + } + + /** + * Set scan method to TCP + * + * @return this object to allow chaining + */ + fun setMethodTCP(): PortScan { + setMethod(METHOD_TCP) + return this + } + + /** Cancel a running ping */ + fun cancel() { + cancelled = true + runningFlowable.unsubscribeOn(Schedulers.computation()) + } + + /** + * Perform a synchronous (blocking) port scan and return a list of open ports + * + * @return - ping result + */ + fun doScan(): List { + cancelled = false + openPortsFound.clear() + runningFlowable = createPortScanFlowable().doOnComplete { + openPortsFound.sort() + } + runningFlowable.blockingSubscribe() + return openPortsFound + } + + private fun createPortScanFlowable(): Flowable { + return Flowable.fromIterable(ports) + .parallel(noThreads) + .runOn(Schedulers.io()) + .map { portNo -> + PortScanRunnable(address, portNo, timeOutMillis, method).run() + }.sequential() + .subscribeOn(Schedulers.computation()) + } + + @Synchronized + private fun portScanned(port: Int, open: Boolean) { + if (open) { + openPortsFound.add(port) + } + portListener?.onResult(port, open) + } + + private inner class PortScanRunnable constructor( + private val address: InetAddress?, + private val portNo: Int, + private val timeOutMillis: Int, + private val method: Int + ) : Runnable { + override fun run() { + if (cancelled) return + when (method) { + METHOD_UDP -> portScanned( + portNo, + PortScanUDP.scanAddress(address, portNo, timeOutMillis) + ) + METHOD_TCP -> portScanned( + portNo, + PortScanTCP.scanAddress(address, portNo, timeOutMillis) + ) + else -> throw IllegalArgumentException("Invalid method") + } + } + } + + companion object { + private const val TIMEOUT_LOCALHOST = 25 + private const val TIMEOUT_LOCALNETWORK = 1000 + private const val TIMEOUT_REMOTE = 2500 + private const val DEFAULT_THREADS_LOCALHOST = 7 + private const val DEFAULT_THREADS_LOCALNETWORK = 50 + private const val DEFAULT_THREADS_REMOTE = 50 + private const val METHOD_TCP = 0 + private const val METHOD_UDP = 1 + + /** + * Set the address to ping + * + * @param address - Address to be pinged + * @return this object to allow chaining + * @throws UnknownHostException - if no IP address for the `host` could be found, or if a + * scope_id was specified for a global IPv6 address. + */ + @JvmStatic + @Throws(UnknownHostException::class) + fun onAddress(address: String?): PortScan { + return onAddress(InetAddress.getByName(address)) + } + + /** + * Set the address to ping + * + * @param ia - Address to be pinged + * @return this object to allow chaining + */ + @JvmStatic + fun onAddress(ia: InetAddress): PortScan { + val portScan = PortScan() + portScan.setAddress(ia) + portScan.setDefaultThreadsAndTimeouts() + return portScan + } + } +} diff --git a/portscanner/src/main/java/com/stealthcopter/networktools/portscanning/PortScanTCP.kt b/portscanner/src/main/java/com/stealthcopter/networktools/portscanning/PortScanTCP.kt new file mode 100644 index 0000000000..8c816b155d --- /dev/null +++ b/portscanner/src/main/java/com/stealthcopter/networktools/portscanning/PortScanTCP.kt @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.stealthcopter.networktools.portscanning + +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Socket + +/** + * Created by mat on 13/12/15. + */ +object PortScanTCP { + /** + * Check if a port is open with TCP + * + * @param ia - address to scan + * @param portNo - port to scan + * @param timeoutMillis - timeout + * @return - true if port is open, false if not or unknown + */ + @JvmStatic + @Suppress("LabeledExpression") + fun scanAddress(ia: InetAddress?, portNo: Int, timeoutMillis: Int): Boolean { + return Socket().let { s -> + runCatching { + s.connect(InetSocketAddress(ia, portNo), timeoutMillis) + return@let true + }.also { + runCatching { + s.close() + }.getOrNull() + }.getOrDefault(false) + } + } +} diff --git a/portscanner/src/main/java/com/stealthcopter/networktools/portscanning/PortScanUDP.kt b/portscanner/src/main/java/com/stealthcopter/networktools/portscanning/PortScanUDP.kt new file mode 100644 index 0000000000..286d6e7e01 --- /dev/null +++ b/portscanner/src/main/java/com/stealthcopter/networktools/portscanning/PortScanUDP.kt @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.stealthcopter.networktools.portscanning + +import java.lang.Exception +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetAddress +import java.net.SocketTimeoutException + +/** + * Created by mat on 13/12/15. + */ +object PortScanUDP { + /** + * Check if a port is open with UDP, note that this isn't reliable + * as UDP will does not send ACKs + * + * @param ia - address to scan + * @param portNo - port to scan + * @param timeoutMillis - timeout + * @return - true if port is open, false if not or unknown + */ + @JvmStatic + fun scanAddress(ia: InetAddress?, portNo: Int, timeoutMillis: Int): Boolean { + try { + val bytes = ByteArray(128) + val dp = DatagramPacket(bytes, bytes.size) + val ds = DatagramSocket() + ds.soTimeout = timeoutMillis + ds.connect(ia, portNo) + ds.send(dp) + ds.isConnected + ds.receive(dp) + ds.close() + } catch (e: SocketTimeoutException) { + return true + } catch (ignore: Exception) { + } + return false + } +} diff --git a/settings.gradle b/settings.gradle index 4f9571f586..70edc43f9d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ include ':file_operations' +include ':portscanner' include ':app', ':commons_compress_7z' diff --git a/testShared/src/test/java/com/amaze/filemanager/shadows/ShadowSmbUtil.kt b/testShared/src/test/java/com/amaze/filemanager/shadows/ShadowSmbUtil.kt index cdc2551c50..b141acd655 100644 --- a/testShared/src/test/java/com/amaze/filemanager/shadows/ShadowSmbUtil.kt +++ b/testShared/src/test/java/com/amaze/filemanager/shadows/ShadowSmbUtil.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.shadows import android.content.Context -import com.amaze.filemanager.utils.SmbUtil +import com.amaze.filemanager.utils.smb.SmbUtil import jcifs.context.SingletonContext import jcifs.smb.SmbException import jcifs.smb.SmbFile diff --git a/testShared/src/test/java/com/amaze/filemanager/test/volley/MockHttpStack.kt b/testShared/src/test/java/com/amaze/filemanager/test/volley/MockHttpStack.kt new file mode 100644 index 0000000000..f859a35b43 --- /dev/null +++ b/testShared/src/test/java/com/amaze/filemanager/test/volley/MockHttpStack.kt @@ -0,0 +1,51 @@ +package com.amaze.filemanager.test.volley + +import com.android.volley.AuthFailureError +import com.android.volley.Request +import com.android.volley.toolbox.BaseHttpStack +import com.android.volley.toolbox.HttpResponse +import java.io.IOException + +/** + * Mock [BaseHttpStack] for test only. + */ +class MockHttpStack : BaseHttpStack() { + + private lateinit var mResponseToReturn: HttpResponse + private lateinit var lastUrl: String + private lateinit var mLastHeaders: MutableMap + private var lastPostBody: ByteArray? = null + + /** + * get headers in last request + */ + fun getLastHeaders() = mLastHeaders + + /** + * Manually set response to return + */ + fun setResponseToReturn(response: HttpResponse) { + mResponseToReturn = response + } + + @Throws(IOException::class, AuthFailureError::class) + override fun executeRequest( + request: Request<*>, + additionalHeaders: Map? + ): HttpResponse { + lastUrl = request.url + mLastHeaders = HashMap() + if (request.headers != null) { + mLastHeaders.putAll(request.headers) + } + if (additionalHeaders != null) { + mLastHeaders.putAll(additionalHeaders) + } + try { + lastPostBody = request.body + } catch (e: AuthFailureError) { + lastPostBody = null + } + return mResponseToReturn + } +} From 8c9b4c61d9a82476c17a1ed1c97e2272de6adf7e Mon Sep 17 00:00:00 2001 From: Obolrom Date: Sun, 4 Jun 2023 18:42:44 +0300 Subject: [PATCH 102/384] Fixed spotlessJavaCheck --- .../ui/fragments/ProcessViewerFragment.java | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/ProcessViewerFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/ProcessViewerFragment.java index b7f221da62..c9f4a181c2 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/ProcessViewerFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/ProcessViewerFragment.java @@ -22,27 +22,8 @@ import static androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT; -import android.content.ComponentName; -import android.content.Intent; -import android.content.ServiceConnection; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.IBinder; -import android.text.Spanned; -import android.text.format.Formatter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.core.text.HtmlCompat; -import androidx.fragment.app.Fragment; +import java.lang.ref.WeakReference; +import java.util.ArrayList; import com.amaze.filemanager.R; import com.amaze.filemanager.asynchronous.services.AbstractProgressiveService; @@ -66,8 +47,27 @@ import com.github.mikephil.charting.data.LineDataSet; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; -import java.lang.ref.WeakReference; -import java.util.ArrayList; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.IBinder; +import android.text.Spanned; +import android.text.format.Formatter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.text.HtmlCompat; +import androidx.fragment.app.Fragment; public class ProcessViewerFragment extends Fragment { @@ -96,7 +96,7 @@ public class ProcessViewerFragment extends Fragment { @Override public View onCreateView( - @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = ProcessparentBinding.inflate(inflater); View rootView = binding.getRoot(); @@ -111,7 +111,8 @@ public View onCreateView( || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { binding.deleteButton.setImageResource(R.drawable.ic_action_cancel); - binding.cardView.setCardBackgroundColor(Utils.getColor(getContext(), R.color.cardView_foreground)); + binding.cardView.setCardBackgroundColor( + Utils.getColor(getContext(), R.color.cardView_foreground)); binding.cardView.setCardElevation(0f); } @@ -124,7 +125,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat mCopyConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_COPY); mExtractConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_EXTRACT); - mCompressConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_COMPRESS); + mCompressConnection = + new CustomServiceConnection(this, binding.progressChart, SERVICE_COMPRESS); mEncryptConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_ENCRYPT); mDecryptConnection = new CustomServiceConnection(this, binding.progressChart, SERVICE_DECRYPT); @@ -267,12 +269,12 @@ private void setupDrawables(int serviceType, boolean isMove) { if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - Drawable copyIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_content_copy_white_36dp); + Drawable copyIcon = + ContextCompat.getDrawable(requireContext(), R.drawable.ic_content_copy_white_36dp); binding.progressImage.setImageDrawable(copyIcon); } else { - Drawable greyCopyIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_content_copy_grey600_36dp); + Drawable greyCopyIcon = + ContextCompat.getDrawable(requireContext(), R.drawable.ic_content_copy_grey600_36dp); binding.progressImage.setImageDrawable(greyCopyIcon); } binding.textViewProgressType.setText( @@ -285,12 +287,12 @@ private void setupDrawables(int serviceType, boolean isMove) { if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - Drawable zipBoxIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_zip_box_white); + Drawable zipBoxIcon = + ContextCompat.getDrawable(requireContext(), R.drawable.ic_zip_box_white); binding.progressImage.setImageDrawable(zipBoxIcon); } else { - Drawable greyZipBoxIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_zip_box_grey); + Drawable greyZipBoxIcon = + ContextCompat.getDrawable(requireContext(), R.drawable.ic_zip_box_grey); binding.progressImage.setImageDrawable(greyZipBoxIcon); } binding.textViewProgressType.setText(getResources().getString(R.string.extracting)); @@ -300,12 +302,12 @@ private void setupDrawables(int serviceType, boolean isMove) { if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - Drawable zipBoxIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_zip_box_white); + Drawable zipBoxIcon = + ContextCompat.getDrawable(requireContext(), R.drawable.ic_zip_box_white); binding.progressImage.setImageDrawable(zipBoxIcon); } else { - Drawable greyZipBoxIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_zip_box_grey); + Drawable greyZipBoxIcon = + ContextCompat.getDrawable(requireContext(), R.drawable.ic_zip_box_grey); binding.progressImage.setImageDrawable(greyZipBoxIcon); } binding.textViewProgressType.setText(getResources().getString(R.string.compressing)); @@ -315,12 +317,12 @@ private void setupDrawables(int serviceType, boolean isMove) { if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - Drawable folderIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_folder_lock_white_36dp); + Drawable folderIcon = + ContextCompat.getDrawable(requireContext(), R.drawable.ic_folder_lock_white_36dp); binding.progressImage.setImageDrawable(folderIcon); } else { - Drawable greyFolderIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_folder_lock_grey600_36dp); + Drawable greyFolderIcon = + ContextCompat.getDrawable(requireContext(), R.drawable.ic_folder_lock_grey600_36dp); binding.progressImage.setImageDrawable(greyFolderIcon); } binding.textViewProgressType.setText(getResources().getString(R.string.crypt_encrypting)); @@ -330,12 +332,14 @@ private void setupDrawables(int serviceType, boolean isMove) { if (mainActivity.getAppTheme().equals(AppTheme.DARK) || mainActivity.getAppTheme().equals(AppTheme.BLACK)) { - Drawable folderUnlockedIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_folder_lock_open_white_36dp); + Drawable folderUnlockedIcon = + ContextCompat.getDrawable( + requireContext(), R.drawable.ic_folder_lock_open_white_36dp); binding.progressImage.setImageDrawable(folderUnlockedIcon); } else { - Drawable greyFolderUnlockedIcon = ContextCompat - .getDrawable(requireContext(), R.drawable.ic_folder_lock_open_grey600_36dp); + Drawable greyFolderUnlockedIcon = + ContextCompat.getDrawable( + requireContext(), R.drawable.ic_folder_lock_open_grey600_36dp); binding.progressImage.setImageDrawable(greyFolderUnlockedIcon); } binding.textViewProgressType.setText(getResources().getString(R.string.crypt_decrypting)); @@ -476,10 +480,9 @@ public void onUpdate(final DatapointParcelable dataPackage) { return; } processViewerFragment - .getActivity() - .runOnUiThread(() -> - processViewerFragment.processResults(dataPackage, serviceType) - ); + .getActivity() + .runOnUiThread( + () -> processViewerFragment.processResults(dataPackage, serviceType)); } } From 15f32a4920fc55f1e2d2ea16704a7bbfa981c957 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Mon, 5 Jun 2023 09:58:31 +0530 Subject: [PATCH 103/384] Minor fix in app manager adapter --- .../java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt index a2188bef0e..fa18dc62db 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt @@ -202,7 +202,7 @@ class AppsRecyclerAdapter( // File f = new File(rowItem.getDesc()); if (!isBottomSheet) { - holder.txtDesc.text = rowItem.fileSize + holder.txtDesc.text = rowItem.fileSize + " |" } holder.rl.isClickable = true holder.rl.nextFocusRightId = holder.about.id From c9fce14fda1f3b778bee1383a21ce674f0320d84 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Mon, 5 Jun 2023 10:20:20 +0530 Subject: [PATCH 104/384] update readme --- CONTRIBUTING.md | 6 +++++- README.md | 13 +++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89ab90e530..3c5c444709 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,9 +23,11 @@ We will then: To speed up the review process, please: -- write your code cleanly. We also have our day time work, we are not able to correct the mistakes in your code +- write your code cleanly. We also have our day time work, we are not able to correct the mistakes in your code. +- before opening PR, run `./gradlew spotlessCheck` and `./gradlew spotlessApply` in your local to check / fix any formatting issues. - describe clearly what your pull request does - if you're fixing a particular bug in the issue list, please explicitly say "Fixes #" in your description +- once you've opened PR, look out for CI builds, if there is any code smell / bug - CI will complain, please work on it to keep the code clean. - while not required (because automated tests can't test everything), it's recommended you to include [Robolectric](http://robolectric.org/)/[Espresso](https://developer.android.com/training/testing/espresso/) tests in your pull request Finally: @@ -35,4 +37,6 @@ Finally: - we can be harsh when reviewing your work but because we want to have good code in our codebase. Be patient and carefully read our comments - we can be wrong. Again, be patient when discuss with us - we accept reason, but we never accept hate speech +If we feel your PR is a significant help to us, we'll award you a bounty with any of your preferred mode of payment. +Please provide the details for the same once asked. Ready to roll? Start forking ;) \ No newline at end of file diff --git a/README.md b/README.md index 3d9682746b..1b00770d9e 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,13 @@ Downloads [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.amaze.filemanager) [Get it on AFH](https://www.androidfilehost.com/?w=files&flid=73967) +Contribute +--- +You can contribute via one of the following ways: +- Help us with the translations - see [Transifex translation](https://www.transifex.com/amaze/amaze-file-manager/). +- [Contribute](https://github.com/TeamAmaze/AmazeFileManager/blob/release/4.0/CONTRIBUTING.md) directly to the code, help us in fixing the bugs / implement new features. +- If we feel your PR is a significant help to us, we'll award you a bounty with any of your preferred mode of payment. + Support --- OpenCollective @@ -67,12 +74,6 @@ Basic r/w operations might not work on external memory on Kitkat devices. Don't See our [Privacy Policy](https://github.com/TeamAmaze/AmazeFileManager/wiki/Privacy-Policy) -Translators ----- -Want to help translate Amaze to your language? - -See [Transifex translation](https://www.transifex.com/amaze/amaze-file-manager/). - Vendors/Developers ---- The device vendors/ROM developers are free to include Amaze apk pre-installed in system. There is no fee required; *but you must comply with the license* (for more information read the [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html) or newer). From c85f577a99d71627971e505abe014015859e3b84 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Mon, 5 Jun 2023 10:22:36 +0530 Subject: [PATCH 105/384] update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b00770d9e..bd941f3713 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ Contribute You can contribute via one of the following ways: - Help us with the translations - see [Transifex translation](https://www.transifex.com/amaze/amaze-file-manager/). - [Contribute](https://github.com/TeamAmaze/AmazeFileManager/blob/release/4.0/CONTRIBUTING.md) directly to the code, help us in fixing the bugs / implement new features. -- If we feel your PR is a significant help to us, we'll award you a bounty with any of your preferred mode of payment. + +_If we feel your contribution is a significant help to us, we'll award you a bounty with any of your preferred mode of payment._ Support --- From e9794b13138a1e874a476bd8493638a01b78e6c1 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Mon, 5 Jun 2023 10:24:15 +0530 Subject: [PATCH 106/384] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd941f3713..79deca1e87 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Downloads Contribute --- You can contribute via one of the following ways: -- Help us with the translations - see [Transifex translation](https://www.transifex.com/amaze/amaze-file-manager/). +- Help us with the translations of either [Amaze File Manager](https://www.transifex.com/amaze/amaze-file-manager/) or [Amaze Utilities](https://crowdin.com/project/amaze-file-utilities) - [Contribute](https://github.com/TeamAmaze/AmazeFileManager/blob/release/4.0/CONTRIBUTING.md) directly to the code, help us in fixing the bugs / implement new features. _If we feel your contribution is a significant help to us, we'll award you a bounty with any of your preferred mode of payment._ From 28edf269a6849829699df42ef1d755cfd8e1b996 Mon Sep 17 00:00:00 2001 From: Mathijs Vogelzang Date: Mon, 5 Jun 2023 16:57:24 +0200 Subject: [PATCH 107/384] Add downloads shield to readme Show on the readme page how popular AmazeFileManager is --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 79deca1e87..6b18d24f5c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ ![CI](https://github.com/TeamAmaze/AmazeFileManager/workflows/Android%20Main%20CI/badge.svg?branch=master) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/9ea2667dabaa4e8c98dbf0876ebacd3e)](https://app.codacy.com/gh/TeamAmaze/AmazeFileManager?utm_source=github.com&utm_medium=referral&utm_content=TeamAmaze/AmazeFileManager&utm_campaign=Badge_Grade_Settings) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/50d8e153feba47b9a8ff82ff57274c56)](https://www.codacy.com/gh/TeamAmaze/AmazeFileManager?utm_source=github.com&utm_medium=referral&utm_content=TeamAmaze/AmazeFileManager&utm_campaign=Badge_Coverage) +[![Amaze File Manager Downloads](https://www.appbrain.com/shield/com.amaze.filemanager.svg)](https://www.appbrain.com/app/amaze-file-manager/com.amaze.filemanager) [![GitHub release](https://img.shields.io/github/release/TeamAmaze/AmazeFileManager.svg)](https://github.com/TeamAmaze/AmazeFileManager/releases) [![IzzyOnDroid](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/com.amaze.filemanager)](https://apt.izzysoft.de/fdroid/index/apk/com.amaze.filemanager) [![Chat on Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/AmazeFileManager) From 72d2774c1cd368e4649603b12afe94c071ead73e Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 6 Jun 2023 22:54:35 +0530 Subject: [PATCH 108/384] move basic search off of main thread Signed-off-by: VishnuSanal --- .../ui/activities/MainActivityViewModel.kt | 38 +++++++++++++++++ .../ui/views/appbar/SearchView.java | 41 ++++++------------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index dc01bebcfd..bc8579a436 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -23,10 +23,17 @@ package com.amaze.filemanager.ui.activities import android.app.Application import androidx.collection.LruCache import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import androidx.preference.PreferenceManager import com.amaze.filemanager.adapters.data.LayoutElementParcelable +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.filesystem.root.ListFilesCommand.listFiles +import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.util.Locale class MainActivityViewModel(val applicationContext: Application) : AndroidViewModel(applicationContext) { @@ -72,4 +79,35 @@ class MainActivityViewModel(val applicationContext: Application) : fun getFromMediaFilesCache(mediaType: Int): List? { return mediaCacheHash[mediaType] } + + fun basicSearch(s: String, mainActivity: MainActivity) : MutableLiveData> { + + val hybridFileParcelables = ArrayList() + + val mutableLiveData: MutableLiveData> = MutableLiveData(hybridFileParcelables) + + val showHiddenFiles = PreferenceManager + .getDefaultSharedPreferences(mainActivity) + .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false) + + viewModelScope.launch(Dispatchers.IO) { + listFiles( + mainActivity.currentMainFragment!!.currentPath!!, + mainActivity.isRootExplorer, + showHiddenFiles, + { _: OpenMode? -> null } + ) { hybridFileParcelable: HybridFileParcelable -> + if (hybridFileParcelable.getName(mainActivity) + .lowercase(Locale.getDefault()) + .contains(s.lowercase(Locale.getDefault())) + && (showHiddenFiles || !hybridFileParcelable.isHidden)) { + hybridFileParcelables.add(hybridFileParcelable) + + mutableLiveData.postValue(hybridFileParcelables) + } + } + } + + return mutableLiveData + } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index f9a2b3e1a3..0aabf3c3a4 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -32,7 +32,6 @@ import com.amaze.filemanager.adapters.SearchRecyclerViewAdapter; import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.filesystem.RootHelper; -import com.amaze.filemanager.filesystem.root.ListFilesCommand; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.theme.AppTheme; @@ -208,14 +207,14 @@ private boolean onSearch(boolean shouldSave) { return false; } - search(s); + basicSearch(s); if (shouldSave) saveRecentPreference(s); return true; } - private void search(String s) { + private void basicSearch(String s) { clearRecyclerView(); @@ -228,30 +227,16 @@ private void search(String s) { mainActivity.getString(R.string.not_finding_what_you_re_looking_for), mainActivity.getString(R.string.try_indexed_search))); - ArrayList hybridFileParcelables = new ArrayList<>(); - - boolean showHiddenFiles = - PreferenceManager.getDefaultSharedPreferences(mainActivity) - .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false); - - // TODO: takes too much resources & freezes main thread on huge folders - ListFilesCommand.INSTANCE.listFiles( - mainActivity.getCurrentMainFragment().getPath(), - mainActivity.isRootExplorer(), - showHiddenFiles, - mode -> null, - hybridFileParcelable -> { - if (hybridFileParcelable.getName(mainActivity).toLowerCase().contains(s.toLowerCase()) - && (showHiddenFiles || !hybridFileParcelable.isHidden())) { - - hybridFileParcelables.add(hybridFileParcelable); - - searchRecyclerViewAdapter.submitList(hybridFileParcelables); - - searchRecyclerViewAdapter.notifyItemInserted(hybridFileParcelables.size() + 1); - } - return null; - }); + mainActivity + .getCurrentMainFragment() + .getMainActivityViewModel() + .basicSearch(s, mainActivity) + .observe( + mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), + hybridFileParcelables -> { + searchRecyclerViewAdapter.submitList(hybridFileParcelables); + searchRecyclerViewAdapter.notifyItemInserted(hybridFileParcelables.size() + 1); + }); } private void saveRecentPreference(String s) { @@ -315,7 +300,7 @@ private void initRecentSearches(Context context) { Utils.hideKeyboard(mainActivity); - search(s); + basicSearch(s); }); } } From db1380718910e59915b7ce7c27ad728749f2a4ee Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 6 Jun 2023 22:56:10 +0530 Subject: [PATCH 109/384] chore: spotless Signed-off-by: VishnuSanal --- .../ui/activities/MainActivityViewModel.kt | 8 ++++---- .../filemanager/ui/views/appbar/SearchView.java | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index bc8579a436..aa7c537759 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -80,8 +80,7 @@ class MainActivityViewModel(val applicationContext: Application) : return mediaCacheHash[mediaType] } - fun basicSearch(s: String, mainActivity: MainActivity) : MutableLiveData> { - + fun basicSearch(s: String, mainActivity: MainActivity): MutableLiveData> { val hybridFileParcelables = ArrayList() val mutableLiveData: MutableLiveData> = MutableLiveData(hybridFileParcelables) @@ -99,8 +98,9 @@ class MainActivityViewModel(val applicationContext: Application) : ) { hybridFileParcelable: HybridFileParcelable -> if (hybridFileParcelable.getName(mainActivity) .lowercase(Locale.getDefault()) - .contains(s.lowercase(Locale.getDefault())) - && (showHiddenFiles || !hybridFileParcelable.isHidden)) { + .contains(s.lowercase(Locale.getDefault())) && + (showHiddenFiles || !hybridFileParcelable.isHidden) + ) { hybridFileParcelables.add(hybridFileParcelable) mutableLiveData.postValue(hybridFileParcelables) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 0aabf3c3a4..53b44d7d15 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -228,12 +228,12 @@ private void basicSearch(String s) { mainActivity.getString(R.string.try_indexed_search))); mainActivity - .getCurrentMainFragment() - .getMainActivityViewModel() - .basicSearch(s, mainActivity) - .observe( - mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), - hybridFileParcelables -> { + .getCurrentMainFragment() + .getMainActivityViewModel() + .basicSearch(s, mainActivity) + .observe( + mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), + hybridFileParcelables -> { searchRecyclerViewAdapter.submitList(hybridFileParcelables); searchRecyclerViewAdapter.notifyItemInserted(hybridFileParcelables.size() + 1); }); From 7573836fadc375ddf11ae037d3175e3a1b3892af Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 6 Jun 2023 23:18:59 +0530 Subject: [PATCH 110/384] move indexed search off of main thread Signed-off-by: VishnuSanal --- .../ui/activities/MainActivityViewModel.kt | 58 ++++++++++++++++++- .../ui/views/appbar/SearchView.java | 51 ++++------------ 2 files changed, 67 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index aa7c537759..103aa2a643 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -21,6 +21,7 @@ package com.amaze.filemanager.ui.activities import android.app.Application +import android.provider.MediaStore import androidx.collection.LruCache import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData @@ -29,10 +30,12 @@ import androidx.preference.PreferenceManager import com.amaze.filemanager.adapters.data.LayoutElementParcelable import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.filesystem.RootHelper import com.amaze.filemanager.filesystem.root.ListFilesCommand.listFiles import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.io.File import java.util.Locale class MainActivityViewModel(val applicationContext: Application) : @@ -80,7 +83,7 @@ class MainActivityViewModel(val applicationContext: Application) : return mediaCacheHash[mediaType] } - fun basicSearch(s: String, mainActivity: MainActivity): MutableLiveData> { + fun basicSearch(mainActivity: MainActivity, query: String): MutableLiveData> { val hybridFileParcelables = ArrayList() val mutableLiveData: MutableLiveData> = MutableLiveData(hybridFileParcelables) @@ -98,7 +101,7 @@ class MainActivityViewModel(val applicationContext: Application) : ) { hybridFileParcelable: HybridFileParcelable -> if (hybridFileParcelable.getName(mainActivity) .lowercase(Locale.getDefault()) - .contains(s.lowercase(Locale.getDefault())) && + .contains(query.lowercase(Locale.getDefault())) && (showHiddenFiles || !hybridFileParcelable.isHidden) ) { hybridFileParcelables.add(hybridFileParcelable) @@ -110,4 +113,55 @@ class MainActivityViewModel(val applicationContext: Application) : return mutableLiveData } + + fun indexedSearch( + mainActivity: MainActivity, + query: String, + ): MutableLiveData< ArrayList > { + + val list = ArrayList() + + val mutableLiveData: MutableLiveData> = MutableLiveData(list) + + val showHiddenFiles = + PreferenceManager.getDefaultSharedPreferences(mainActivity) + .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false) + + viewModelScope.launch(Dispatchers.IO) { + + val projection = arrayOf(MediaStore.Files.FileColumns.DATA) + + val cursor = mainActivity + .contentResolver + .query(MediaStore.Files.getContentUri("external"), projection, null, null, null) + ?: return@launch + + if (cursor.count > 0 && cursor.moveToFirst()) { + do { + val path = + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)) + + if (path != null + && path.contains(mainActivity.currentMainFragment?.currentPath!!) + && File(path).name.lowercase(Locale.getDefault()).contains( + query.lowercase(Locale.getDefault()) + ) + ) { + + val hybridFileParcelable = + RootHelper.generateBaseFile(File(path), showHiddenFiles) + + if (hybridFileParcelable != null) { + list.add(hybridFileParcelable) + mutableLiveData.postValue(list) + } + } + } while (cursor.moveToNext()) + } + + cursor.close() + } + + return mutableLiveData + } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 53b44d7d15..4a513b2825 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -172,10 +172,16 @@ public void afterTextChanged(Editable s) {} if (searchMode == 1) { - List hybridFileParcelables = indexedSearch(mainActivity, s); - - searchRecyclerViewAdapter.submitList(hybridFileParcelables); - searchRecyclerViewAdapter.notifyDataSetChanged(); + mainActivity + .getCurrentMainFragment() + .getMainActivityViewModel() + .indexedSearch(mainActivity, s) + .observe( + mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), + hybridFileParcelables -> { + searchRecyclerViewAdapter.submitList(hybridFileParcelables); + searchRecyclerViewAdapter.notifyDataSetChanged(); + }); searchMode = 2; deepSearchTV.setText( @@ -230,7 +236,7 @@ private void basicSearch(String s) { mainActivity .getCurrentMainFragment() .getMainActivityViewModel() - .basicSearch(s, mainActivity) + .basicSearch(mainActivity, s) .observe( mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), hybridFileParcelables -> { @@ -305,41 +311,6 @@ private void initRecentSearches(Context context) { } } - private List indexedSearch(MainActivity mainActivity, String query) { - - ArrayList list = new ArrayList<>(); - final String[] projection = {MediaStore.Files.FileColumns.DATA}; - - Cursor cursor = - mainActivity - .getContentResolver() - .query(MediaStore.Files.getContentUri("external"), projection, null, null, null); - - if (cursor == null) return list; - else if (cursor.getCount() > 0 && cursor.moveToFirst()) { - do { - String path = - cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)); - - if (path != null - && path.contains(mainActivity.getCurrentMainFragment().getPath()) - && new File(path).getName().toLowerCase().contains(query.toLowerCase())) { - - boolean showHiddenFiles = - PreferenceManager.getDefaultSharedPreferences(mainActivity) - .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false); - - HybridFileParcelable hybridFileParcelable = - RootHelper.generateBaseFile(new File(path), showHiddenFiles); - - if (hybridFileParcelable != null) list.add(hybridFileParcelable); - } - } while (cursor.moveToNext()); - } - cursor.close(); - return list; - } - /** show search view with a circular reveal animation */ public void revealSearchView() { final int START_RADIUS = 16; From febaf038d2ab8d138fd2830729454a976bd35942 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 6 Jun 2023 23:22:04 +0530 Subject: [PATCH 111/384] chore: spotless Signed-off-by: VishnuSanal --- .../ui/activities/MainActivityViewModel.kt | 13 ++++----- .../ui/views/appbar/SearchView.java | 27 +++++++------------ 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index 103aa2a643..eb90938ae4 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -116,9 +116,8 @@ class MainActivityViewModel(val applicationContext: Application) : fun indexedSearch( mainActivity: MainActivity, - query: String, - ): MutableLiveData< ArrayList > { - + query: String + ): MutableLiveData> { val list = ArrayList() val mutableLiveData: MutableLiveData> = MutableLiveData(list) @@ -128,7 +127,6 @@ class MainActivityViewModel(val applicationContext: Application) : .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false) viewModelScope.launch(Dispatchers.IO) { - val projection = arrayOf(MediaStore.Files.FileColumns.DATA) val cursor = mainActivity @@ -141,13 +139,12 @@ class MainActivityViewModel(val applicationContext: Application) : val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)) - if (path != null - && path.contains(mainActivity.currentMainFragment?.currentPath!!) - && File(path).name.lowercase(Locale.getDefault()).contains( + if (path != null && + path.contains(mainActivity.currentMainFragment?.currentPath!!) && + File(path).name.lowercase(Locale.getDefault()).contains( query.lowercase(Locale.getDefault()) ) ) { - val hybridFileParcelable = RootHelper.generateBaseFile(File(path), showHiddenFiles) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 4a513b2825..b6ba9910a9 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -22,16 +22,11 @@ import static android.content.Context.INPUT_METHOD_SERVICE; import static android.os.Build.VERSION.SDK_INT; -import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES; -import java.io.File; import java.util.ArrayList; -import java.util.List; import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.SearchRecyclerViewAdapter; -import com.amaze.filemanager.filesystem.HybridFileParcelable; -import com.amaze.filemanager.filesystem.RootHelper; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.theme.AppTheme; @@ -45,9 +40,7 @@ import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; -import android.database.Cursor; import android.graphics.PorterDuff; -import android.provider.MediaStore; import android.text.Editable; import android.text.TextWatcher; import android.view.ContextThemeWrapper; @@ -172,16 +165,16 @@ public void afterTextChanged(Editable s) {} if (searchMode == 1) { - mainActivity - .getCurrentMainFragment() - .getMainActivityViewModel() - .indexedSearch(mainActivity, s) - .observe( - mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), - hybridFileParcelables -> { - searchRecyclerViewAdapter.submitList(hybridFileParcelables); - searchRecyclerViewAdapter.notifyDataSetChanged(); - }); + mainActivity + .getCurrentMainFragment() + .getMainActivityViewModel() + .indexedSearch(mainActivity, s) + .observe( + mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), + hybridFileParcelables -> { + searchRecyclerViewAdapter.submitList(hybridFileParcelables); + searchRecyclerViewAdapter.notifyDataSetChanged(); + }); searchMode = 2; deepSearchTV.setText( From 1cb7fd48a293f1306d46041ddf4c7889ee2e6b1f Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 6 Jun 2023 23:24:04 +0530 Subject: [PATCH 112/384] chore: spotless Signed-off-by: VishnuSanal --- .../ui/activities/MainActivityViewModel.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index eb90938ae4..32aecb7282 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -83,10 +83,13 @@ class MainActivityViewModel(val applicationContext: Application) : return mediaCacheHash[mediaType] } - fun basicSearch(mainActivity: MainActivity, query: String): MutableLiveData> { + fun basicSearch(mainActivity: MainActivity, query: String): + MutableLiveData> { val hybridFileParcelables = ArrayList() - val mutableLiveData: MutableLiveData> = MutableLiveData(hybridFileParcelables) + val mutableLiveData: + MutableLiveData> = + MutableLiveData(hybridFileParcelables) val showHiddenFiles = PreferenceManager .getDefaultSharedPreferences(mainActivity) @@ -100,8 +103,8 @@ class MainActivityViewModel(val applicationContext: Application) : { _: OpenMode? -> null } ) { hybridFileParcelable: HybridFileParcelable -> if (hybridFileParcelable.getName(mainActivity) - .lowercase(Locale.getDefault()) - .contains(query.lowercase(Locale.getDefault())) && + .lowercase(Locale.getDefault()) + .contains(query.lowercase(Locale.getDefault())) && (showHiddenFiles || !hybridFileParcelable.isHidden) ) { hybridFileParcelables.add(hybridFileParcelable) @@ -120,7 +123,9 @@ class MainActivityViewModel(val applicationContext: Application) : ): MutableLiveData> { val list = ArrayList() - val mutableLiveData: MutableLiveData> = MutableLiveData(list) + val mutableLiveData: MutableLiveData> = MutableLiveData( + list + ) val showHiddenFiles = PreferenceManager.getDefaultSharedPreferences(mainActivity) @@ -137,13 +142,15 @@ class MainActivityViewModel(val applicationContext: Application) : if (cursor.count > 0 && cursor.moveToFirst()) { do { val path = - cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)) + cursor.getString( + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA) + ) if (path != null && path.contains(mainActivity.currentMainFragment?.currentPath!!) && File(path).name.lowercase(Locale.getDefault()).contains( - query.lowercase(Locale.getDefault()) - ) + query.lowercase(Locale.getDefault()) + ) ) { val hybridFileParcelable = RootHelper.generateBaseFile(File(path), showHiddenFiles) From 402934a6fb6defacab4e6f76c76850c1594710ba Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Wed, 7 Jun 2023 21:14:56 +0530 Subject: [PATCH 113/384] minor change to app manager UI Signed-off-by: VishnuSanal --- .../java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt | 1 - .../java/com/amaze/filemanager/adapters/holders/AppHolder.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt index fa18dc62db..60460b7c2b 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt @@ -68,7 +68,6 @@ import com.amaze.filemanager.utils.AnimUtils.marqueeAfterDelay import com.amaze.filemanager.utils.Utils import com.amaze.filemanager.utils.safeLet import java.io.File -import kotlin.collections.ArrayList import kotlin.math.roundToInt class AppsRecyclerAdapter( diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt index b14091a4c0..97bbd696b1 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt @@ -60,7 +60,7 @@ class AppHolder(view: View) : RecyclerView.ViewHolder(view) { packageName.visibility = View.VISIBLE val layoutParams = txtDesc.layoutParams as ViewGroup.MarginLayoutParams - layoutParams.setMargins(txtDesc.marginLeft, txtDesc.marginTop, 8, txtDesc.marginBottom) + layoutParams.setMargins(txtDesc.marginLeft, txtDesc.marginTop, 4, txtDesc.marginBottom) txtDesc.layoutParams = layoutParams view.findViewById(R.id.picture_icon).visibility = View.GONE From adbeec0475f6b74eef5074026d37099c956336ce Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Wed, 7 Jun 2023 23:46:13 +0530 Subject: [PATCH 114/384] codacy: functions missing docs Signed-off-by: VishnuSanal --- .../filemanager/ui/activities/MainActivityViewModel.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index 32aecb7282..81192a0d18 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -83,6 +83,9 @@ class MainActivityViewModel(val applicationContext: Application) : return mediaCacheHash[mediaType] } + /** + * Perform basic search: searches on the current directory + */ fun basicSearch(mainActivity: MainActivity, query: String): MutableLiveData> { val hybridFileParcelables = ArrayList() @@ -117,6 +120,9 @@ class MainActivityViewModel(val applicationContext: Application) : return mutableLiveData } + /** + * Perform indexed search: on MediaStore items from the current directory & it's children + */ fun indexedSearch( mainActivity: MainActivity, query: String From 3e7c53db85eb47dc9397c9bf8493023cf175590e Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Wed, 7 Jun 2023 23:52:33 +0530 Subject: [PATCH 115/384] fix: glitch when searching for the second time Signed-off-by: VishnuSanal --- .../ui/views/appbar/SearchView.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index b6ba9910a9..8664f1034c 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -107,14 +107,6 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener deepSearchTV = mainActivity.findViewById(R.id.searchDeepSearchTV); recyclerView = mainActivity.findViewById(R.id.searchRecyclerView); - searchMode = 0; - deepSearchTV.setText( - String.format( - "%s %s", - mainActivity.getString(R.string.not_finding_what_you_re_looking_for), - mainActivity.getString(R.string.try_indexed_search))); - deepSearchTV.setVisibility(View.GONE); - initRecentSearches(mainActivity); searchRecyclerViewAdapter = new SearchRecyclerViewAdapter(); @@ -304,11 +296,23 @@ private void initRecentSearches(Context context) { } } + private void resetSearchMode() { + searchMode = 0; + deepSearchTV.setText( + String.format( + "%s %s", + mainActivity.getString(R.string.not_finding_what_you_re_looking_for), + mainActivity.getString(R.string.try_indexed_search))); + deepSearchTV.setVisibility(View.GONE); + } + /** show search view with a circular reveal animation */ public void revealSearchView() { final int START_RADIUS = 16; int endRadius = Math.max(appbar.getToolbar().getWidth(), appbar.getToolbar().getHeight()); + resetSearchMode(); + Animator animator; if (SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { int[] searchCoords = new int[2]; From 82afd3bf81d6a411c42570cb8f4822b1f5bdec9f Mon Sep 17 00:00:00 2001 From: Hafis Date: Thu, 8 Jun 2023 20:56:01 +0530 Subject: [PATCH 116/384] first commit --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c5c444709..05e527963b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Please keep in mind the points below before considering contributing to Amaze: You won't be able to claim the license for changes made by you unless you do that. If there's no license header in any file, please include one from [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html) webpage. - Please follow [Android/JAVA code style](https://source.android.com/source/code-style.html) for writing any code, but do not use the Hungarian notation proposed - discussion [here](https://github.com/TeamAmaze/AmazeFileManager/issues/986). -Also, follow [Android Material Design guidelines](https://material.io/guidelines/material-design/introduction.html) in case you make changes to any UI element. +Also, follow [Android Material Design guidelines](https://m2.material.io/design/introduction) in case you make changes to any UI element. - To file a bug report, it is recommended to include the steps to reproduce it; and even better, it helps us a lot if you can capture the error messages in logcat too It is also recommended to enroll to our beta program from Play Store to test and verify any fix for the same. @@ -39,4 +39,4 @@ Finally: If we feel your PR is a significant help to us, we'll award you a bounty with any of your preferred mode of payment. Please provide the details for the same once asked. -Ready to roll? Start forking ;) \ No newline at end of file +Ready to roll? Start forking ;) From 0b0306e76b8c80fe078dd2463da2bee6b438df5b Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 8 Jun 2023 23:08:31 +0530 Subject: [PATCH 117/384] search: change "recursive" to "deep" Signed-off-by: VishnuSanal --- .../com/amaze/filemanager/ui/views/appbar/SearchView.java | 8 ++++---- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 8664f1034c..b3763b29c3 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -83,9 +83,9 @@ public class SearchView { private final SearchRecyclerViewAdapter searchRecyclerViewAdapter; - // 0 -> Basic - // 1 -> Indexed - // 2 -> Recursive + // 0 -> Basic Search + // 1 -> Indexed Search + // 2 -> Deep Search private int searchMode; private boolean enabled = false; @@ -173,7 +173,7 @@ public void afterTextChanged(Editable s) {} String.format( "%s %s", mainActivity.getString(R.string.not_finding_what_you_re_looking_for), - mainActivity.getString(R.string.try_recursive_search))); + mainActivity.getString(R.string.try_deep_search))); } else if (searchMode == 2) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0dcc2662e7..8e71c95b5a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -810,7 +810,7 @@ You only need to do this once, until the next time you select a new location for Cloud Connection credentials cleared Unfortunately, we were unable to migrate your cloud connection credentials to the new database schema, and we had to remove them from the app. Please create the cloud connection again. Not finding what you\'re looking for? - Try Recursive Search! + Try Deep Search! Try Indexed Search! Recent Results From e48a7808d1f0173ed5fd9751aa7dfd901350429b Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 8 Jun 2023 23:28:14 +0530 Subject: [PATCH 118/384] fix crash (seems like the issue was caused by not clearing the RecyclerView when a new dataset needs to be listed) (https://stackoverflow.com/q/35653439/9652621) Signed-off-by: VishnuSanal --- .../java/com/amaze/filemanager/ui/views/appbar/SearchView.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index b3763b29c3..0f12d79c13 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -112,8 +112,6 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener searchRecyclerViewAdapter = new SearchRecyclerViewAdapter(); recyclerView.setAdapter(searchRecyclerViewAdapter); - clearRecyclerView(); - clearImageView.setOnClickListener( v -> { searchViewEditText.setText(""); @@ -312,6 +310,7 @@ public void revealSearchView() { int endRadius = Math.max(appbar.getToolbar().getWidth(), appbar.getToolbar().getHeight()); resetSearchMode(); + clearRecyclerView(); Animator animator; if (SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { From 27e5507c6a37444ef3ab7ed2967a8b753b3500be Mon Sep 17 00:00:00 2001 From: Hafis CP <75908249+CPHafis@users.noreply.github.com> Date: Fri, 9 Jun 2023 11:47:07 +0530 Subject: [PATCH 119/384] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05e527963b..2ed71c7119 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Please keep in mind the points below before considering contributing to Amaze: You won't be able to claim the license for changes made by you unless you do that. If there's no license header in any file, please include one from [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html) webpage. - Please follow [Android/JAVA code style](https://source.android.com/source/code-style.html) for writing any code, but do not use the Hungarian notation proposed - discussion [here](https://github.com/TeamAmaze/AmazeFileManager/issues/986). -Also, follow [Android Material Design guidelines](https://m2.material.io/design/introduction) in case you make changes to any UI element. +Also, follow [Android Material Design guidelines](https://m3.material.io/get-started) in case you make changes to any UI element. - To file a bug report, it is recommended to include the steps to reproduce it; and even better, it helps us a lot if you can capture the error messages in logcat too It is also recommended to enroll to our beta program from Play Store to test and verify any fix for the same. From ec00564acb0f97bc2d87eb3f19446490d5d14e6e Mon Sep 17 00:00:00 2001 From: Hafis CP <75908249+CPHafis@users.noreply.github.com> Date: Fri, 9 Jun 2023 11:48:01 +0530 Subject: [PATCH 120/384] Update CONTRIBUTING.md From 9be28fc73815c75dad5d58754cd43fbfa15ad3c5 Mon Sep 17 00:00:00 2001 From: Hafis CP <75908249+CPHafis@users.noreply.github.com> Date: Fri, 9 Jun 2023 20:50:28 +0530 Subject: [PATCH 121/384] Update CONTRIBUTING.md From b8f5c4ae80c5a1f53bade8a2ebf5976625834663 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 12 Jun 2023 00:19:51 +0530 Subject: [PATCH 122/384] move deep search TV to top Signed-off-by: VishnuSanal --- app/src/main/res/layout-v21/layout_search.xml | 42 +++++++++--------- .../main/res/layout-w720dp/layout_search.xml | 43 ++++++++++--------- app/src/main/res/layout/layout_search.xml | 42 +++++++++--------- 3 files changed, 66 insertions(+), 61 deletions(-) diff --git a/app/src/main/res/layout-v21/layout_search.xml b/app/src/main/res/layout-v21/layout_search.xml index ed8aaa8dcb..b5ce1cdc1b 100644 --- a/app/src/main/res/layout-v21/layout_search.xml +++ b/app/src/main/res/layout-v21/layout_search.xml @@ -81,9 +81,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:animateLayoutChanges="true" - android:padding="8dp" + android:paddingStart="8dp" + android:paddingTop="8dp" + android:paddingEnd="8dp" android:scrollbars="none" - app:layout_constraintBottom_toTopOf="@id/searchResultsHintTV" + app:layout_constraintBottom_toTopOf="@id/searchDeepSearchTV" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/searchRecentHintTV"> @@ -101,6 +103,22 @@ + + + app:layout_constraintTop_toBottomOf="@id/searchDeepSearchTV" /> - - + app:layout_constraintTop_toBottomOf="@id/searchResultsHintTV" /> \ No newline at end of file diff --git a/app/src/main/res/layout-w720dp/layout_search.xml b/app/src/main/res/layout-w720dp/layout_search.xml index e7360f67db..177e166eb1 100644 --- a/app/src/main/res/layout-w720dp/layout_search.xml +++ b/app/src/main/res/layout-w720dp/layout_search.xml @@ -78,9 +78,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:animateLayoutChanges="true" - android:padding="8dp" + android:paddingStart="8dp" + android:paddingTop="8dp" + android:paddingEnd="8dp" android:scrollbars="none" - app:layout_constraintBottom_toTopOf="@id/searchResultsHintTV" + app:layout_constraintBottom_toTopOf="@id/searchDeepSearchTV" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/searchRecentHintTV"> @@ -98,6 +100,22 @@ + + - + app:layout_constraintTop_toBottomOf="@id/searchDeepSearchTV" /> - - + app:layout_constraintTop_toBottomOf="@id/searchResultsHintTV" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_search.xml b/app/src/main/res/layout/layout_search.xml index f6c646f9d2..650b33bca0 100644 --- a/app/src/main/res/layout/layout_search.xml +++ b/app/src/main/res/layout/layout_search.xml @@ -78,9 +78,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:animateLayoutChanges="true" - android:padding="8dp" + android:paddingStart="8dp" + android:paddingTop="8dp" + android:paddingEnd="8dp" android:scrollbars="none" - app:layout_constraintBottom_toTopOf="@id/searchResultsHintTV" + app:layout_constraintBottom_toTopOf="@id/searchDeepSearchTV" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/searchRecentHintTV"> @@ -98,6 +100,22 @@ + + + app:layout_constraintTop_toBottomOf="@id/searchDeepSearchTV" /> - - + app:layout_constraintTop_toBottomOf="@id/searchResultsHintTV" /> \ No newline at end of file From f41e169f22a80e3a6313e9a18b59639a8ec06691 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Mon, 12 Jun 2023 00:34:52 +0530 Subject: [PATCH 123/384] apply accent color to deep search prompt Signed-off-by: VishnuSanal --- .../ui/views/appbar/SearchView.java | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 0f12d79c13..794c560025 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -41,8 +41,13 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PorterDuff; +import android.graphics.Typeface; import android.text.Editable; +import android.text.Spannable; +import android.text.SpannableString; import android.text.TextWatcher; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.view.ContextThemeWrapper; import android.view.View; import android.view.ViewAnimationUtils; @@ -167,9 +172,9 @@ public void afterTextChanged(Editable s) {} }); searchMode = 2; + deepSearchTV.setText( - String.format( - "%s %s", + getSpannableText( mainActivity.getString(R.string.not_finding_what_you_re_looking_for), mainActivity.getString(R.string.try_deep_search))); @@ -211,8 +216,7 @@ private void basicSearch(String s) { deepSearchTV.setVisibility(View.VISIBLE); searchMode = 1; deepSearchTV.setText( - String.format( - "%s %s", + getSpannableText( mainActivity.getString(R.string.not_finding_what_you_re_looking_for), mainActivity.getString(R.string.try_indexed_search))); @@ -297,8 +301,7 @@ private void initRecentSearches(Context context) { private void resetSearchMode() { searchMode = 0; deepSearchTV.setText( - String.format( - "%s %s", + getSpannableText( mainActivity.getString(R.string.not_finding_what_you_re_looking_for), mainActivity.getString(R.string.try_indexed_search))); deepSearchTV.setVisibility(View.GONE); @@ -310,7 +313,7 @@ public void revealSearchView() { int endRadius = Math.max(appbar.getToolbar().getWidth(), appbar.getToolbar().getHeight()); resetSearchMode(); - clearRecyclerView(); + clearRecyclerView(); Animator animator; if (SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { @@ -461,6 +464,24 @@ private void clearRecyclerView() { searchResultsHintTV.setVisibility(View.GONE); } + private SpannableString getSpannableText(String s1, String s2) { + + SpannableString spannableString = new SpannableString(s1 + " " + s2); + + spannableString.setSpan( + new ForegroundColorSpan(mainActivity.getCurrentColorPreference().getAccent()), + s1.length() + 1, + spannableString.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spannableString.setSpan( + new StyleSpan(Typeface.BOLD), + s1.length() + 1, + spannableString.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return spannableString; + } + public interface SearchListener { void onSearch(String queue); } From e0a052ad78a6465464e77d1a03140ccb25451d91 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 13 Jun 2023 21:14:47 +0530 Subject: [PATCH 124/384] add to recents upon indexed search Signed-off-by: VishnuSanal --- .../java/com/amaze/filemanager/ui/views/appbar/SearchView.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 794c560025..bbe3a77ac7 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -160,6 +160,8 @@ public void afterTextChanged(Editable s) {} if (searchMode == 1) { + saveRecentPreference(s); + mainActivity .getCurrentMainFragment() .getMainActivityViewModel() From df0eaadc0c4cf74c8fec6289e46f45c2bd08ddc3 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Tue, 13 Jun 2023 21:20:28 +0530 Subject: [PATCH 125/384] use the color view on the left to distinguish between a file & a directory Signed-off-by: VishnuSanal --- .../adapters/SearchRecyclerViewAdapter.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt index ad17c6caa9..c523453385 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt @@ -69,14 +69,14 @@ class SearchRecyclerViewAdapter : holder.colorView.setBackgroundColor(getRandomColor(holder.colorView.context)) -// val colorPreference = -// (AppConfig.getInstance().mainActivityContext as MainActivity).currentColorPreference -// -// if (item != null && item.isDirectory) { // always false for some reason! -// holder.colorView.setBackgroundColor(colorPreference.iconSkin) -// } else { -// holder.colorView.setBackgroundColor(colorPreference.accent) -// } + val colorPreference = + (AppConfig.getInstance().mainActivityContext as MainActivity).currentColorPreference + + if (item.isDirectory) { + holder.colorView.setBackgroundColor(colorPreference.primaryFirstTab) + } else { + holder.colorView.setBackgroundColor(colorPreference.accent) + } } inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { From f23522e9b82d01f3649e41a81ab3847aaf7e9074 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Wed, 14 Jun 2023 22:16:32 +0530 Subject: [PATCH 126/384] return the actual path for local files Signed-off-by: VishnuSanal --- .../java/com/amaze/filemanager/filesystem/HybridFile.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 8f55be3882..8e69b2d97d 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -416,11 +416,15 @@ public Long execute(@NonNull SFTPClient client) throws IOException { } /** - * Path accessor. Avoid direct access to path since path may have been URL encoded. + * Path accessor. Avoid direct access to path (for non-local files) since path may have been URL + * encoded. * - * @return URL decoded path + * @return URL decoded path (for non-local files); the actual path for local files */ public String getPath() { + + if (isLocal() || isRoot() || isDocumentFile() || isAndroidDataDir()) return path; + try { return URLDecoder.decode(path.replace("+", "%2b"), "UTF-8"); } catch (UnsupportedEncodingException | IllegalArgumentException e) { From f11f6a9d1f21a2b5451a2532ecef4db1e1886c90 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Wed, 14 Jun 2023 22:24:01 +0530 Subject: [PATCH 127/384] remove the workaround done to replace plus symbol (via #3661) Signed-off-by: VishnuSanal --- .../main/java/com/amaze/filemanager/filesystem/HybridFile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8e69b2d97d..13d9c40bf8 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -426,7 +426,7 @@ public String getPath() { if (isLocal() || isRoot() || isDocumentFile() || isAndroidDataDir()) return path; try { - return URLDecoder.decode(path.replace("+", "%2b"), "UTF-8"); + return URLDecoder.decode(path, "UTF-8"); } catch (UnsupportedEncodingException | IllegalArgumentException e) { LOG.warn("failed to decode path {}", path, e); return path; From 354f3cfa2be4b61578158fbf8665f79b53314c53 Mon Sep 17 00:00:00 2001 From: Obolrom Date: Wed, 14 Jun 2023 23:59:07 +0300 Subject: [PATCH 128/384] Refactoring ZipService: Migrated compressAsyncTask to RxJava chain --- .../asynchronous/services/ZipService.kt | 103 ++++++++++-------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt index 43d4471d0a..537c5dfae0 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt @@ -24,7 +24,6 @@ import android.app.PendingIntent import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.* import android.net.Uri -import android.os.AsyncTask import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.O import android.os.IBinder @@ -45,6 +44,12 @@ import com.amaze.filemanager.ui.notifications.NotificationConstants import com.amaze.filemanager.utils.DatapointParcelable import com.amaze.filemanager.utils.ObtainableServiceBinder import com.amaze.filemanager.utils.ProgressHandler +import io.reactivex.Completable +import io.reactivex.CompletableEmitter +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.* @@ -62,7 +67,7 @@ class ZipService : AbstractProgressiveService() { private val log: Logger = LoggerFactory.getLogger(ZipService::class.java) private val mBinder: IBinder = ObtainableServiceBinder(this) - private lateinit var asyncTask: CompressAsyncTask + private val disposables = CompositeDisposable() private lateinit var mNotifyManager: NotificationManagerCompat private lateinit var mBuilder: NotificationCompat.Builder private var progressListener: ProgressListener? = null @@ -139,8 +144,8 @@ class ZipService : AbstractProgressiveService() { initNotificationViews() super.onStartCommand(intent, flags, startId) super.progressHalted() - asyncTask = CompressAsyncTask(this, baseFiles, mZipPath!!) - asyncTask.execute() + val zipTask = CompressTask(this, baseFiles, mZipPath!!) + disposables.add(zipTask.compress()) // If we get killed, after returning from here, restart return START_NOT_STICKY } @@ -170,59 +175,70 @@ class ZipService : AbstractProgressiveService() { override fun clearDataPackages() = dataPackages.clear() - inner class CompressAsyncTask( + inner class CompressTask( private val zipService: ZipService, private val baseFiles: ArrayList, private val zipPath: String - ) : AsyncTask() { + ) { private lateinit var zos: ZipOutputStream private lateinit var watcherUtil: ServiceWatcherUtil - private var totalBytes = 0L - - override fun doInBackground(vararg p1: Void): Void? { - // setting up service watchers and initial data packages - // finding total size on background thread (this is necessary condition for SMB!) - totalBytes = FileUtils.getTotalBytes(baseFiles, zipService.applicationContext) - progressHandler.sourceSize = baseFiles.size - progressHandler.totalSize = totalBytes - progressHandler.setProgressListener { speed: Long -> - publishResults(speed, false, false) + + fun compress(): Disposable { + return Completable.create { emitter -> + // setting up service watchers and initial data packages + // finding total size on background thread (this is necessary condition for SMB!) + val totalBytes = FileUtils.getTotalBytes(baseFiles, zipService.applicationContext) + progressHandler.sourceSize = baseFiles.size + progressHandler.totalSize = totalBytes + + progressHandler.setProgressListener { speed: Long -> + publishResults(speed, false, false) + } + zipService.addFirstDatapoint( + baseFiles[0].getName(applicationContext), + baseFiles.size, + totalBytes, + false + ) + execute( + emitter, + zipService.applicationContext, + FileUtils.hybridListToFileArrayList(baseFiles), + zipPath + ) + + emitter.onComplete() } - zipService.addFirstDatapoint( - baseFiles[0].getName(applicationContext), - baseFiles.size, - totalBytes, - false - ) - execute( - zipService.applicationContext, - FileUtils.hybridListToFileArrayList(baseFiles), - zipPath - ) - - return null + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + watcherUtil.stopWatch() + val intent = Intent(MainActivity.KEY_INTENT_LOAD_LIST) + .putExtra(MainActivity.KEY_INTENT_LOAD_LIST_FILE, zipPath) + zipService.sendBroadcast(intent) + zipService.stopSelf() + }, + { log.error(it.message ?: "ZipService.CompressAsyncTask.compress failed") } + ) } - override fun onCancelled() { - super.onCancelled() + fun cancel() { progressHandler.cancelled = true val zipFile = File(zipPath) if (zipFile.exists()) zipFile.delete() } - public override fun onPostExecute(a: Void?) { - watcherUtil.stopWatch() - val intent = Intent(MainActivity.KEY_INTENT_LOAD_LIST) - .putExtra(MainActivity.KEY_INTENT_LOAD_LIST_FILE, zipPath) - zipService.sendBroadcast(intent) - zipService.stopSelf() - } - /** * Main logic for zipping specified files. */ - fun execute(context: Context, baseFiles: ArrayList, zipPath: String?) { + fun execute( + emitter: CompletableEmitter, + context: Context, + baseFiles: ArrayList, + zipPath: String, + ) { val out: OutputStream? val zipDirectory = File(zipPath) watcherUtil = ServiceWatcherUtil(progressHandler) @@ -231,7 +247,7 @@ class ZipService : AbstractProgressiveService() { out = FileUtil.getOutputStream(zipDirectory, context) zos = ZipOutputStream(BufferedOutputStream(out)) for ((fileProgress, file) in baseFiles.withIndex()) { - if (isCancelled) return + if (emitter.isDisposed) return progressHandler.fileName = file.name progressHandler.sourceFilesProcessed = fileProgress + 1 compressFile(file, "") @@ -259,8 +275,8 @@ class ZipService : AbstractProgressiveService() { zos.putNextEntry(createZipEntry(file, path)) val buf = ByteArray(GenericCopyUtil.DEFAULT_BUFFER_SIZE) var len: Int - BufferedInputStream(FileInputStream(file)).use { `in` -> - while (`in`.read(buf).also { len = it } > 0) { + BufferedInputStream(FileInputStream(file)).use { bufferedInputStream -> + while (bufferedInputStream.read(buf).also { len = it } > 0) { if (!progressHandler.cancelled) { zos.write(buf, 0, len) ServiceWatcherUtil.position += len.toLong() @@ -312,6 +328,7 @@ class ZipService : AbstractProgressiveService() { override fun onDestroy() { super.onDestroy() unregisterReceiver(receiver1) + disposables.dispose() } companion object { From 4baa4fdb8e6117c6c2d0b0f2d064eebd5c313bcf Mon Sep 17 00:00:00 2001 From: Obolrom Date: Thu, 15 Jun 2023 15:40:32 +0300 Subject: [PATCH 129/384] Refactoring ZipService: fixed filepath warning --- .../com/amaze/filemanager/asynchronous/services/ZipService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt index 537c5dfae0..7b03b6a08e 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt @@ -144,7 +144,7 @@ class ZipService : AbstractProgressiveService() { initNotificationViews() super.onStartCommand(intent, flags, startId) super.progressHalted() - val zipTask = CompressTask(this, baseFiles, mZipPath!!) + val zipTask = CompressTask(this, baseFiles, zipFile.absolutePath) disposables.add(zipTask.compress()) // If we get killed, after returning from here, restart return START_NOT_STICKY From 29f7e5762f6e00c28a58b4f024e5e9fc2f3a5e6b Mon Sep 17 00:00:00 2001 From: Obolrom Date: Thu, 15 Jun 2023 15:46:22 +0300 Subject: [PATCH 130/384] Refactoring ZipService: fixed spotless --- .../com/amaze/filemanager/asynchronous/services/ZipService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt index 7b03b6a08e..c7d3e2e382 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt @@ -237,7 +237,7 @@ class ZipService : AbstractProgressiveService() { emitter: CompletableEmitter, context: Context, baseFiles: ArrayList, - zipPath: String, + zipPath: String ) { val out: OutputStream? val zipDirectory = File(zipPath) From c28afd37b24400000e03b6b9770a93b72029072a Mon Sep 17 00:00:00 2001 From: Obolrom Date: Thu, 15 Jun 2023 19:06:00 +0300 Subject: [PATCH 131/384] Refactoring ZipService: added documentation for new methods --- .../amaze/filemanager/asynchronous/services/ZipService.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt index c7d3e2e382..2109d92461 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ZipService.kt @@ -184,6 +184,9 @@ class ZipService : AbstractProgressiveService() { private lateinit var zos: ZipOutputStream private lateinit var watcherUtil: ServiceWatcherUtil + /** + * Main use case for executing zipping task by given [zipPath] + */ fun compress(): Disposable { return Completable.create { emitter -> // setting up service watchers and initial data packages @@ -224,6 +227,9 @@ class ZipService : AbstractProgressiveService() { ) } + /** + * Deletes the destination file zip file if exists + */ fun cancel() { progressHandler.cancelled = true val zipFile = File(zipPath) From 38f795422a1dd8c0a5ecd40e6a3452592041f2d3 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Thu, 15 Jun 2023 23:29:27 +0530 Subject: [PATCH 132/384] fix broken media scanner Signed-off-by: VishnuSanal --- .../asynchronous/asynctasks/DeleteTask.java | 24 +----- .../asynctasks/movecopy/MoveFilesTask.kt | 6 +- .../asynchronous/services/CopyService.java | 3 +- .../filemanager/filesystem/Operations.java | 3 +- .../filesystem/files/FileUtils.java | 76 ------------------- .../filesystem/files/GenericCopyUtil.java | 2 +- .../filesystem/files/MediaConnectionUtils.kt | 53 +++++++++++++ .../ui/fragments/MainFragment.java | 3 +- 8 files changed, 67 insertions(+), 103 deletions(-) create mode 100644 app/src/main/java/com/amaze/filemanager/filesystem/files/MediaConnectionUtils.kt diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java index 15ec67cace..c63bbe3ef6 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java @@ -38,7 +38,7 @@ import com.amaze.filemanager.filesystem.SafRootHolder; import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.CryptUtil; -import com.amaze.filemanager.filesystem.files.FileUtils; +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.fragments.CompressedExplorerFragment; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; @@ -48,12 +48,9 @@ import com.cloudrail.si.interfaces.CloudStorage; import android.app.NotificationManager; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.AsyncTask; -import android.provider.MediaStore; import android.widget.Toast; import androidx.annotation.NonNull; @@ -112,13 +109,9 @@ protected final AsyncTaskResult doInBackground( } // delete file from media database - if (!file.isSmb()) { - try { - deleteFromMediaDatabase(applicationContext, file.getPath()); - } catch (Exception e) { - FileUtils.scanFile(applicationContext, files.toArray(new HybridFile[files.size()])); - } - } + if (!file.isSmb()) + MediaConnectionUtils.scanFile( + applicationContext, files.toArray(new HybridFile[files.size()])); // delete file entry from encrypted database if (file.getName(applicationContext).endsWith(CryptUtil.CRYPT_EXTENSION)) { @@ -194,13 +187,4 @@ private boolean doDeleteFile(@NonNull HybridFileParcelable file) throws Exceptio } } } - - private void deleteFromMediaDatabase(final Context context, final String file) { - final String where = MediaStore.MediaColumns.DATA + "=?"; - final String[] selectionArgs = new String[] {file}; - final ContentResolver contentResolver = context.getContentResolver(); - final Uri filesUri = MediaStore.Files.getContentUri("external"); - // Delete the entry from the media database. This will actually delete media files. - contentResolver.delete(filesUri, where, selectionArgs); - } } diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFilesTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFilesTask.kt index 196f05d668..988cfb8bde 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFilesTask.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFilesTask.kt @@ -34,7 +34,7 @@ import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFile import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.filesystem.files.CryptUtil -import com.amaze.filemanager.filesystem.files.FileUtils +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils import com.amaze.filemanager.ui.activities.MainActivity import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -106,8 +106,8 @@ class MoveFilesTask( for (hybridFileParcelables in files) { sourcesFiles.addAll(hybridFileParcelables) } - FileUtils.scanFile(applicationContext, sourcesFiles.toTypedArray()) - FileUtils.scanFile(applicationContext, targetFiles.toTypedArray()) + MediaConnectionUtils.scanFile(applicationContext, sourcesFiles.toTypedArray()) + MediaConnectionUtils.scanFile(applicationContext, targetFiles.toTypedArray()) } // updating encrypted db entry if any encrypted file was moved diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java index e83fdd4af7..4ca5b3eeec 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java @@ -43,6 +43,7 @@ import com.amaze.filemanager.filesystem.files.CryptUtil; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.files.GenericCopyUtil; +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; import com.amaze.filemanager.filesystem.root.CopyFilesCommand; import com.amaze.filemanager.filesystem.root.MoveFileCommand; import com.amaze.filemanager.ui.activities.MainActivity; @@ -467,7 +468,7 @@ void copyRoot(HybridFileParcelable sourceFile, HybridFile targetFile, boolean mo e); failedFOps.add(sourceFile); } - FileUtils.scanFile(c, new HybridFile[] {targetFile}); + MediaConnectionUtils.scanFile(c, new HybridFile[] {targetFile}); } private void copyFiles( diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java b/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java index 4125f726eb..6786b1efa2 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java @@ -41,6 +41,7 @@ import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.FileUtils; +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; import com.amaze.filemanager.filesystem.ftp.FtpClientTemplate; import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils; import com.amaze.filemanager.filesystem.root.MakeDirectoryCommand; @@ -715,7 +716,7 @@ protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); if (newFile != null && oldFile != null) { HybridFile[] hybridFiles = {newFile, oldFile}; - FileUtils.scanFile(context, hybridFiles); + MediaConnectionUtils.scanFile(context, hybridFiles); } } }.executeOnExecutor(executor); diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java index d55e7f09b3..4fcf97cb81 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java @@ -30,7 +30,6 @@ import java.util.Date; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; @@ -42,7 +41,6 @@ import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.fileoperations.filesystem.smbstreamer.Streamer; -import com.amaze.filemanager.filesystem.ExternalSdCardOperation; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.filesystem.Operations; @@ -71,7 +69,6 @@ import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.annotation.SuppressLint; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; @@ -79,7 +76,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.media.MediaScannerConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -92,8 +88,6 @@ import androidx.core.util.Pair; import androidx.documentfile.provider.DocumentFile; -import io.reactivex.Flowable; -import io.reactivex.schedulers.Schedulers; import jcifs.smb.SmbFile; import kotlin.collections.ArraysKt; import net.schmizz.sshj.sftp.RemoteResourceInfo; @@ -217,76 +211,6 @@ public static long getBaseFileSize(HybridFileParcelable baseFile, Context contex } } - /** - * Triggers media scanner for multiple paths. The paths must all belong to same filesystem. It's - * upto the caller to call the mediastore scan on multiple files or only one source/target - * directory. Don't use filesystem API directly as files might not be present anymore (eg. - * move/rename) which may lead to {@link java.io.FileNotFoundException} - * - * @param hybridFiles - * @param context - */ - @SuppressLint("CheckResult") - public static void scanFile(@NonNull Context context, @NonNull HybridFile[] hybridFiles) { - Flowable.fromCallable( - (Callable) - () -> { - if (hybridFiles[0].exists(context) && hybridFiles[0].isLocal()) { - String[] paths = new String[hybridFiles.length]; - for (int i = 0; i < hybridFiles.length; i++) { - HybridFile hybridFile = hybridFiles[i]; - paths[i] = hybridFile.getPath(); - } - MediaScannerConnection.scanFile(context, paths, null, null); - } - for (HybridFile hybridFile : hybridFiles) { - scanFile(hybridFile, context); - } - return null; - }) - .subscribeOn(Schedulers.io()); - } - - /** - * Triggers media store for the file path - * - * @param hybridFile the file which was changed (directory not supported) - * @param context given context - */ - private static void scanFile(@NonNull HybridFile hybridFile, Context context) { - - if ((hybridFile.isLocal() || hybridFile.isOtgFile()) && hybridFile.exists(context)) { - - Uri uri = null; - if (Build.VERSION.SDK_INT >= 19) { - DocumentFile documentFile = - ExternalSdCardOperation.getDocumentFile( - hybridFile.getFile(), hybridFile.isDirectory(context), context); - // If FileUtil.getDocumentFile() returns null, fall back to DocumentFile.fromFile() - if (documentFile == null) documentFile = DocumentFile.fromFile(hybridFile.getFile()); - uri = documentFile.getUri(); - } else { - if (hybridFile.isLocal()) { - uri = Uri.fromFile(hybridFile.getFile()); - } - } - if (uri != null) { - FileUtils.scanFile(uri, context); - } - } - } - - /** - * Triggers {@link Intent#ACTION_MEDIA_SCANNER_SCAN_FILE} intent to refresh the media store. - * - * @param uri File's {@link Uri} - * @param c {@link Context} - */ - private static void scanFile(@NonNull Uri uri, @NonNull Context c) { - Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri); - c.sendBroadcast(mediaScanIntent); - } - public static void crossfade(View buttons, final View pathbar) { // Set the content view to 0% opacity but visible, so that it is visible // (but fully transparent) during the animation. diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java index 068d6a95fe..0f09d9cc49 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java @@ -271,7 +271,7 @@ private void startCopy( // If target file is copied onto the device and copy was successful, trigger media store // rescan if (mTargetFile != null) { - FileUtils.scanFile(mContext, new HybridFile[] {mTargetFile}); + MediaConnectionUtils.scanFile(mContext, new HybridFile[] {mTargetFile}); } } } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/MediaConnectionUtils.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/MediaConnectionUtils.kt new file mode 100644 index 0000000000..dadb5744e3 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/MediaConnectionUtils.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2014-2022 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.filesystem.files + +import android.content.Context +import android.media.MediaScannerConnection +import android.net.Uri +import android.util.Log +import com.amaze.filemanager.filesystem.HybridFile + +object MediaConnectionUtils { + private val TAG = MediaConnectionUtils::class.java.simpleName + + /** + * Invokes MediaScannerConnection#scanFile for the given files + * + * @param context the context + * @param hybridFiles files to be scanned + */ + @JvmStatic + fun scanFile(context: Context, hybridFiles: Array) { + val paths = arrayOfNulls(hybridFiles.size) + + for (i in hybridFiles.indices) paths[i] = hybridFiles[i].path + + MediaScannerConnection.scanFile( + context, + paths, + null + ) { + path: String, _: Uri? -> + Log.i(TAG, "public scanFile() Finished scanning path$path") + } + } +} 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 5abc495d5e..c74b9cd7e9 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 @@ -63,6 +63,7 @@ import com.amaze.filemanager.filesystem.files.EncryptDecryptUtils; import com.amaze.filemanager.filesystem.files.FileListSorter; import com.amaze.filemanager.filesystem.files.FileUtils; +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; import com.amaze.filemanager.ui.ExtensionsKt; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.activities.MainActivityViewModel; @@ -1401,7 +1402,7 @@ public void hide(String path) { LOG.warn("failure when hiding file", e); } } - FileUtils.scanFile( + MediaConnectionUtils.scanFile( requireMainActivity(), new HybridFile[] {new HybridFile(OpenMode.FILE, path)}); } } From 6f52b0c515561a17959868f234afa2b3792c6e02 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 24 Sep 2022 17:39:48 +0800 Subject: [PATCH 133/384] Subnetscanner v2 Fixes #2622 and fixes #3386. --- .../filemanager/ui/dialogs/SmbSearchDialog.kt | 15 ++++++++++++--- .../utils/smb/SameSubnetDiscoverDeviceStrategy.kt | 11 ++++++++++- .../utils/smb/SmbDeviceScannerObservable.kt | 6 +++++- .../filemanager/ui/activities/MainActivityTest.kt | 4 ++-- .../ui/dialogs/SmbConnectDialogTest.kt | 2 +- portscanner/src/main/AndroidManifest.xml | 8 +------- 6 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt index bd92520db8..014f5b5d3b 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt @@ -152,7 +152,12 @@ class SmbSearchDialog : DialogFragment() { * Called by [ComputerParcelableViewModel], add found computer to list view */ fun add(computer: ComputerParcelable) { - items.add(computer) + if (computer.addr == "-1" && computer.name == "-1") { + items.add(computer) + } else { + items.add(items.size - 1, computer) + removeDummy() + } notifyDataSetChanged() } @@ -161,7 +166,11 @@ class SmbSearchDialog : DialogFragment() { * (dummy) host */ fun removeDummy() { - items.removeFirst() + items.remove( + items.find { + it.addr == "-1" && it.name == "-1" + } + ) notifyDataSetChanged() } @@ -176,7 +185,7 @@ class SmbSearchDialog : DialogFragment() { * Answers if the list is empty = only has the dummy [ComputerParcelable] instance */ fun dummyOnly(): Boolean { - return items.size == 1 && items.first().addr == "-1" + return items.size == 1 && items.last().addr == "-1" } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { diff --git a/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt index 277150c647..d4385fe95a 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt @@ -80,7 +80,16 @@ class SameSubnetDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDevi it is InetAddress }.doOnNext { addr -> addr as InetAddress - callback.invoke(ComputerParcelable(addr.hostAddress, addr.hostName)) + callback.invoke( + ComputerParcelable( + addr.hostAddress, + if (addr.hostName == addr.hostAddress) { + addr.canonicalHostName + } else { + addr.hostName + } + ) + ) }.sequential().subscribe() } diff --git a/app/src/main/java/com/amaze/filemanager/utils/smb/SmbDeviceScannerObservable.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/SmbDeviceScannerObservable.kt index 14dcced9d8..0d4afd52c4 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/smb/SmbDeviceScannerObservable.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/SmbDeviceScannerObservable.kt @@ -20,6 +20,7 @@ package com.amaze.filemanager.utils.smb +import androidx.annotation.VisibleForTesting import com.amaze.filemanager.utils.ComputerParcelable import com.amaze.filemanager.utils.smb.SmbDeviceScannerObservable.DiscoverDeviceStrategy import io.reactivex.Observable @@ -50,11 +51,14 @@ class SmbDeviceScannerObservable : Observable() { fun onCancel() } - private var discoverDeviceStrategies: Array = + var discoverDeviceStrategies: Array = arrayOf( WsddDiscoverDeviceStrategy(), SameSubnetDiscoverDeviceStrategy() ) + @VisibleForTesting set + + @VisibleForTesting get private lateinit var observer: Observer diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.kt index 3743edfbca..79aeb00fa5 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityTest.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import com.amaze.filemanager.application.AppConfig -import com.amaze.filemanager.utils.SmbUtil.getSmbDecryptedPath -import com.amaze.filemanager.utils.SmbUtil.getSmbEncryptedPath +import com.amaze.filemanager.utils.smb.SmbUtil.getSmbDecryptedPath +import com.amaze.filemanager.utils.smb.SmbUtil.getSmbEncryptedPath import org.awaitility.Awaitility.await import org.junit.Assert.assertEquals import org.junit.Test diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialogTest.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialogTest.kt index e2d7a4be17..dd76331050 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialogTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/SmbConnectDialogTest.kt @@ -32,7 +32,7 @@ import com.amaze.filemanager.ui.dialogs.SmbConnectDialog.ARG_EDIT import com.amaze.filemanager.ui.dialogs.SmbConnectDialog.ARG_NAME import com.amaze.filemanager.ui.dialogs.SmbConnectDialog.ARG_PATH import com.amaze.filemanager.ui.dialogs.SmbConnectDialog.SmbConnectionListener -import com.amaze.filemanager.utils.SmbUtil +import com.amaze.filemanager.utils.smb.SmbUtil import io.mockk.confirmVerified import io.mockk.spyk import io.mockk.verify diff --git a/portscanner/src/main/AndroidManifest.xml b/portscanner/src/main/AndroidManifest.xml index 100ab263c3..568741e54f 100644 --- a/portscanner/src/main/AndroidManifest.xml +++ b/portscanner/src/main/AndroidManifest.xml @@ -1,8 +1,2 @@ - - - - - \ No newline at end of file + \ No newline at end of file From e536f9f37a64204748087822f51e2ae92d7c7d23 Mon Sep 17 00:00:00 2001 From: VishnuSanal Date: Fri, 23 Jun 2023 21:01:58 +0530 Subject: [PATCH 134/384] use Utils#dpToPx Signed-off-by: VishnuSanal --- .../com/amaze/filemanager/adapters/holders/AppHolder.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt index 97bbd696b1..f5ef96a37f 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt @@ -32,6 +32,7 @@ import androidx.core.view.marginTop import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R import com.amaze.filemanager.ui.views.ThemedTextView +import com.amaze.filemanager.utils.Utils class AppHolder(view: View) : RecyclerView.ViewHolder(view) { @JvmField @@ -60,7 +61,12 @@ class AppHolder(view: View) : RecyclerView.ViewHolder(view) { packageName.visibility = View.VISIBLE val layoutParams = txtDesc.layoutParams as ViewGroup.MarginLayoutParams - layoutParams.setMargins(txtDesc.marginLeft, txtDesc.marginTop, 4, txtDesc.marginBottom) + layoutParams.setMargins( + txtDesc.marginLeft, + txtDesc.marginTop, + Utils.dpToPx(view.context, 4), + txtDesc.marginBottom + ) txtDesc.layoutParams = layoutParams view.findViewById(R.id.picture_icon).visibility = View.GONE From 6bd56f439e324ff60cd5eb5eb356dc1e52a86cd9 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 24 Jun 2023 17:37:07 +0800 Subject: [PATCH 135/384] Adds Compress option to item popup menu Fixes #3766. --- .../com/amaze/filemanager/adapters/RecyclerAdapter.java | 5 ++++- .../java/com/amaze/filemanager/ui/ItemPopupMenu.java | 6 ++++++ .../filemanager/ui/dialogs/GeneralDialogCreation.java | 9 +++++++++ app/src/main/res/menu/item_extras.xml | 3 +++ 4 files changed, 22 insertions(+), 1 deletion(-) 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 047aadfba0..7e180188ae 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -1392,6 +1392,7 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl } } else { popupMenu.getMenu().findItem(R.id.book).setVisible(false); + popupMenu.getMenu().findItem(R.id.compress).setVisible(true); if (description.endsWith(fileExtensionZip) || description.endsWith(fileExtensionJar) @@ -1409,8 +1410,10 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl || description.endsWith(fileExtensionGz) || description.endsWith(fileExtensionBzip2) || description.endsWith(fileExtensionLzma) - || description.endsWith(fileExtensionXz)) + || description.endsWith(fileExtensionXz)) { popupMenu.getMenu().findItem(R.id.ex).setVisible(true); + popupMenu.getMenu().findItem(R.id.compress).setVisible(false); + } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { diff --git a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java index 1785f87d5a..a3ede65503 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java +++ b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java @@ -241,6 +241,12 @@ public void onButtonPressed(Intent intent, String password) utilitiesProvider, false); return true; + case R.id.compress: + GeneralDialogCreation.showCompressDialog( + mainActivity, + rowItem.generateBaseFile(), + mainActivity.getCurrentMainFragment().getMainFragmentViewModel().getCurrentPath()); + return true; case R.id.return_select: mainFragment.returnIntentResults(rowItem.generateBaseFile()); return true; 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 c7e917c0a6..d952d3edde 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 @@ -867,6 +867,15 @@ public static void showArchiveDialog(final File f, final MainActivity m) { b.show(); } + public static void showCompressDialog( + @NonNull final MainActivity mainActivity, + final HybridFileParcelable baseFile, + final String current) { + ArrayList baseFiles = new ArrayList<>(); + baseFiles.add(baseFile); + showCompressDialog(mainActivity, baseFiles, current); + } + public static void showCompressDialog( @NonNull final MainActivity mainActivity, final ArrayList baseFiles, diff --git a/app/src/main/res/menu/item_extras.xml b/app/src/main/res/menu/item_extras.xml index acc508b5dc..d2d2d1201e 100644 --- a/app/src/main/res/menu/item_extras.xml +++ b/app/src/main/res/menu/item_extras.xml @@ -39,6 +39,9 @@ + From 5107b2ab9c98f37ab567aa266dac9bc54ac99c84 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Tue, 27 Jun 2023 23:09:38 +0800 Subject: [PATCH 136/384] Remove set drawer header background and Volley dependency We are used to allow setting the drawer's background image when we still had a Google+ community. Then we disabled it and the code was left cold and dead for quite a while. This PR removes the dead code and Volley dependency altogether, paving the way for OkHttp as the HTTP client library to use within Amaze, as well as the blockade towards implementing WebDAV client support(#376) --- app/build.gradle | 2 - .../filemanager/application/AppConfig.java | 17 ----- .../ui/activities/MainActivity.java | 6 +- .../filemanager/ui/views/drawer/Drawer.java | 52 --------------- .../filemanager/utils/LruBitmapCache.java | 63 ------------------- .../application/AppConfigTest.java | 13 ---- 6 files changed, 2 insertions(+), 151 deletions(-) delete mode 100644 app/src/main/java/com/amaze/filemanager/utils/LruBitmapCache.java diff --git a/app/build.gradle b/app/build.gradle index 34cd3e4b97..ecb040a502 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,8 +187,6 @@ dependencies { implementation 'org.greenrobot:eventbus:3.3.1' - implementation 'com.android.volley:volley:1.2.1' - implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" implementation "com.github.topjohnwu.libsu:io:${libsuVersion}" 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 09a33595f9..0c0c18379f 100644 --- a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java +++ b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java @@ -41,11 +41,7 @@ import com.amaze.filemanager.database.UtilsHandler; import com.amaze.filemanager.filesystem.ssh.CustomSshJConfig; import com.amaze.filemanager.ui.provider.UtilitiesProvider; -import com.amaze.filemanager.utils.LruBitmapCache; import com.amaze.filemanager.utils.ScreenUtils; -import com.android.volley.RequestQueue; -import com.android.volley.toolbox.ImageLoader; -import com.android.volley.toolbox.Volley; import android.app.Activity; import android.app.Application; @@ -71,8 +67,6 @@ public class AppConfig extends GlideApplication { private Logger log = null; private UtilitiesProvider utilsProvider; - private RequestQueue requestQueue; - private ImageLoader imageLoader; private UtilsHandler utilsHandler; private WeakReference mainActivityContext; @@ -201,17 +195,6 @@ public static synchronized AppConfig getInstance() { return instance; } - public ImageLoader getImageLoader() { - if (requestQueue == null) { - requestQueue = Volley.newRequestQueue(getApplicationContext()); - } - - if (imageLoader == null) { - this.imageLoader = new ImageLoader(requestQueue, new LruBitmapCache()); - } - return imageLoader; - } - public UtilsHandler getUtilsHandler() { return utilsHandler; } 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 04cd312614..d268f41ad1 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 @@ -1539,9 +1539,7 @@ public Drawer getDrawer() { protected void onActivityResult(int requestCode, int responseCode, Intent intent) { super.onActivityResult(requestCode, responseCode, intent); - if (requestCode == Drawer.image_selector_request_code) { - drawer.onActivityResult(requestCode, responseCode, intent); - } else if (requestCode == 3) { + if (requestCode == 3) { Uri treeUri; if (responseCode == Activity.RESULT_OK) { // Get Uri from Storage Access Framework. @@ -1721,7 +1719,7 @@ void initialiseViews() { if (getAppbar().getSearchView().isEnabled()) getAppbar().getSearchView().hideSearchView(); }); - drawer.setDrawerHeaderBackground(); + // drawer.setDrawerHeaderBackground(); } /** diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java index c55fb95000..90a98a38a5 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java @@ -20,7 +20,6 @@ package com.amaze.filemanager.ui.views.drawer; -import static android.os.Build.VERSION.SDK_INT; import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTPS_URI_PREFIX; import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.FTP_URI_PREFIX; import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX; @@ -57,7 +56,6 @@ import com.amaze.filemanager.ui.fragments.CloudSheetFragment; import com.amaze.filemanager.ui.fragments.FtpServerFragment; import com.amaze.filemanager.ui.fragments.MainFragment; -import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.fragments.preferencefragments.QuickAccessesPrefsFragment; import com.amaze.filemanager.ui.theme.AppTheme; import com.amaze.filemanager.utils.Billing; @@ -68,8 +66,6 @@ import com.amaze.filemanager.utils.ScreenUtils; import com.amaze.filemanager.utils.TinyDB; import com.amaze.filemanager.utils.Utils; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.ImageLoader; import com.cloudrail.si.interfaces.CloudStorage; import com.cloudrail.si.services.Box; import com.cloudrail.si.services.Dropbox; @@ -113,8 +109,6 @@ public class Drawer implements NavigationView.OnNavigationItemSelectedListener { private static final Logger LOG = LoggerFactory.getLogger(Drawer.class); - public static final int image_selector_request_code = 31; - public static final int STORAGES_GROUP = 0, SERVERS_GROUP = 1, CLOUDS_GROUP = 2, @@ -134,7 +128,6 @@ public class Drawer implements NavigationView.OnNavigationItemSelectedListener { private boolean isDrawerLocked = false; private FragmentTransaction pending_fragmentTransaction; private String pendingPath; - private ImageLoader mImageLoader; private String firstPath = null, secondPath = null; private DrawerLayout mDrawerLayout; @@ -185,8 +178,6 @@ public Drawer(MainActivity mainActivity) { return false; });*/ - mImageLoader = AppConfig.getInstance().getImageLoader(); - navView = mainActivity.findViewById(R.id.navigation); // set width of drawer in portrait to follow material guidelines @@ -742,23 +733,6 @@ private void addNewItem( } } - public void onActivityResult(int requestCode, int responseCode, Intent intent) { - if (mainActivity.getPrefs() != null && intent != null && intent.getData() != null) { - if (SDK_INT >= Build.VERSION_CODES.KITKAT) { - mainActivity - .getContentResolver() - .takePersistableUriPermission(intent.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - mainActivity - .getPrefs() - .edit() - .putString( - PreferencesConstants.PREFERENCE_DRAWER_HEADER_PATH, intent.getData().toString()) - .commit(); - setDrawerHeaderBackground(); - } - } - public void closeIfNotLocked() { if (!isLocked()) { close(); @@ -921,32 +895,6 @@ public int getPhoneStorageCount() { return phoneStorageCount; } - public void setDrawerHeaderBackground() { - String path1 = - mainActivity.getPrefs().getString(PreferencesConstants.PREFERENCE_DRAWER_HEADER_PATH, null); - if (path1 == null) { - return; - } - try { - final ImageView headerImageView = new ImageView(mainActivity); - headerImageView.setImageDrawable(drawerHeaderParent.getBackground()); - mImageLoader.get( - path1, - new ImageLoader.ImageListener() { - @Override - public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) { - headerImageView.setImageBitmap(response.getBitmap()); - drawerHeaderView.setBackgroundResource(R.drawable.amaze_header_2); - } - - @Override - public void onErrorResponse(VolleyError error) {} - }); - } catch (Exception e) { - LOG.warn("failed to set drawer header background", e); - } - } - public void selectCorrectDrawerItemForPath(final String path) { Integer id = dataUtils.findLongestContainingDrawerItem(path); diff --git a/app/src/main/java/com/amaze/filemanager/utils/LruBitmapCache.java b/app/src/main/java/com/amaze/filemanager/utils/LruBitmapCache.java deleted file mode 100644 index 66e2103690..0000000000 --- a/app/src/main/java/com/amaze/filemanager/utils/LruBitmapCache.java +++ /dev/null @@ -1,63 +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.utils; - -import com.android.volley.toolbox.ImageLoader; - -import android.graphics.Bitmap; -import android.util.LruCache; - -/** Created by vishal on 7/6/16. */ -public class LruBitmapCache extends LruCache implements ImageLoader.ImageCache { - /** - * @param maxSize for caches that do not override {@link #sizeOf}, this is the maximum number of - * entries in the cache. For all other caches, this is the maximum sum of the sizes of the - * entries in this cache. - */ - public LruBitmapCache(int maxSize) { - super(maxSize); - } - - public LruBitmapCache() { - this(getDefaultCacheSize()); - } - - private static int getDefaultCacheSize() { - int memory = (int) (Runtime.getRuntime().maxMemory() / 1024); - return memory / 8; - } - - @Override - public Bitmap getBitmap(String url) { - return get(url); - } - - @Override - public void putBitmap(String url, Bitmap bitmap) { - put(url, bitmap); - } - - @Override - protected int sizeOf(String key, Bitmap value) { - - return value.getByteCount(); - } -} diff --git a/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java b/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java index b92d24725a..84ea2611b4 100644 --- a/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java +++ b/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java @@ -90,19 +90,6 @@ public void testToastWithString() { assertEquals("Hello world", ShadowToast.getTextOfLatestToast()); } - @Test - public void testGetImageLoader() throws Exception { - Field requestQueue = AppConfig.class.getDeclaredField("requestQueue"); - Field imageLoader = AppConfig.class.getDeclaredField("imageLoader"); - requestQueue.setAccessible(true); - imageLoader.setAccessible(true); - - assertNull(requestQueue.get(AppConfig.getInstance())); - assertNull(imageLoader.get(AppConfig.getInstance())); - - assertNotNull(AppConfig.getInstance().getImageLoader()); - } - @Test public void testGlideMemoryCategorySetToHigh() throws Exception { Field memoryCategory = Glide.class.getDeclaredField("memoryCategory"); From 230313292d6936321985574b337f40791c277744 Mon Sep 17 00:00:00 2001 From: Obolrom Date: Sat, 1 Jul 2023 20:50:58 +0300 Subject: [PATCH 137/384] ExtractService minor code improvements --- .../asynchronous/services/ExtractService.java | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java index 48a0d9fff9..74f426a53e 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java @@ -24,7 +24,6 @@ import java.io.File; import java.io.IOException; -import java.lang.ref.WeakReference; import java.util.ArrayList; import org.apache.commons.compress.PasswordRequiredException; @@ -59,6 +58,7 @@ import android.widget.RemoteViews; import android.widget.Toast; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; @@ -70,15 +70,14 @@ public class ExtractService extends AbstractProgressiveService { private final IBinder mBinder = new ObtainableServiceBinder<>(this); // list of data packages,// to initiate chart in process viewer fragment - private ArrayList dataPackages = new ArrayList<>(); + private final ArrayList dataPackages = new ArrayList<>(); private NotificationManagerCompat mNotifyManager; private NotificationCompat.Builder mBuilder; - private ProgressHandler progressHandler = new ProgressHandler(); + private final ProgressHandler progressHandler = new ProgressHandler(); private ProgressListener progressListener; - private int accentColor; - private SharedPreferences sharedPreferences; private RemoteViews customSmallContentViews, customBigContentViews; + private @Nullable DoWork extractingAsyncTask; public static final String KEY_PATH_ZIP = "zip"; public static final String KEY_ENTRIES_ZIP = "entries"; @@ -98,8 +97,9 @@ public int onStartCommand(Intent intent, int flags, final int startId) { String[] entries = intent.getStringArrayExtra(KEY_ENTRIES_ZIP); mNotifyManager = NotificationManagerCompat.from(getApplicationContext()); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - accentColor = + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + int accentColor = ((AppConfig) getApplication()) .getUtilsProvider() .getColorPreference() @@ -153,7 +153,8 @@ public int onStartCommand(Intent intent, int flags, final int startId) { super.onStartCommand(intent, flags, startId); super.progressHalted(); - new DoWork(this, progressHandler, file, extractPath, entries).execute(); + extractingAsyncTask = new DoWork(progressHandler, file, extractPath, entries); + extractingAsyncTask.execute(); return START_NOT_STICKY; } @@ -216,6 +217,9 @@ protected void clearDataPackages() { @Override public void onDestroy() { super.onDestroy(); + if (extractingAsyncTask != null) { + extractingAsyncTask.cancel(true); + } unregisterReceiver(receiver1); } @@ -228,21 +232,15 @@ private long getTotalSize(String filePath) { } public class DoWork extends AsyncTask { - private WeakReference extractService; private String[] entriesToExtract; - private String extractionPath, compressedPath; - private ProgressHandler progressHandler; + private String extractionPath; + private final String compressedPath; + private final ProgressHandler progressHandler; private ServiceWatcherUtil watcherUtil; private boolean paused = false; private boolean passwordProtected = false; - private DoWork( - ExtractService extractService, - ProgressHandler progressHandler, - String cpath, - String epath, - String[] entries) { - this.extractService = new WeakReference<>(extractService); + private DoWork(ProgressHandler progressHandler, String cpath, String epath, String[] entries) { this.progressHandler = progressHandler; compressedPath = cpath; extractionPath = epath; @@ -254,8 +252,7 @@ protected Boolean doInBackground(Void... p) { while (!isCancelled()) { if (paused) continue; - final ExtractService extractService = this.extractService.get(); - if (extractService == null) return null; + final ExtractService extractService = ExtractService.this; File f = new File(compressedPath); String extractDirName = CompressedHelper.getFileName(f.getName()); @@ -396,7 +393,7 @@ protected void onProgressUpdate(IOException... values) { (dialog, which) -> { EditText editText = dialog.getView().findViewById(R.id.singleedittext_input); ArchivePasswordCache.getInstance().put(compressedPath, editText.getText().toString()); - this.extractService.get().getDataPackages().clear(); + ExtractService.this.getDataPackages().clear(); this.paused = false; dialog.dismiss(); }, @@ -413,8 +410,7 @@ protected void onProgressUpdate(IOException... values) { @Override public void onPostExecute(Boolean hasInvalidEntries) { ArchivePasswordCache.getInstance().remove(compressedPath); - final ExtractService extractService = this.extractService.get(); - if (extractService == null) return; + final ExtractService extractService = ExtractService.this; // check whether watcherutil was initialized. It was not initialized when we got exception // in extracting the file @@ -453,7 +449,7 @@ private void toastOnParseError(IOException result) { * Class used for the client Binder. Because we know this service always runs in the same process * as its clients, we don't need to deal with IPC. */ - private BroadcastReceiver receiver1 = + private final BroadcastReceiver receiver1 = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { From 380f89d888a2f023fb326248a02ae75297e8dff4 Mon Sep 17 00:00:00 2001 From: Obolrom Date: Sat, 1 Jul 2023 20:59:22 +0300 Subject: [PATCH 138/384] CopyService minor code improvements --- .../asynchronous/services/CopyService.java | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java index e83fdd4af7..fbb0ae012a 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java @@ -85,18 +85,12 @@ public class CopyService extends AbstractProgressiveService { private final IBinder mBinder = new ObtainableServiceBinder<>(this); private ServiceWatcherUtil watcherUtil; - private ProgressHandler progressHandler = new ProgressHandler(); + private final ProgressHandler progressHandler = new ProgressHandler(); private ProgressListener progressListener; // list of data packages, to initiate chart in process viewer fragment - private ArrayList dataPackages = new ArrayList<>(); - private int accentColor; - private SharedPreferences sharedPreferences; + private final ArrayList dataPackages = new ArrayList<>(); private RemoteViews customSmallContentViews, customBigContentViews; - private boolean isRootExplorer; - private long totalSize = 0L; - private int totalSourceFiles = 0; - @Override public void onCreate() { super.onCreate(); @@ -107,13 +101,13 @@ public void onCreate() { @Override public int onStartCommand(Intent intent, int flags, final int startId) { Bundle b = new Bundle(); - isRootExplorer = intent.getBooleanExtra(TAG_IS_ROOT_EXPLORER, false); + boolean isRootExplorer = intent.getBooleanExtra(TAG_IS_ROOT_EXPLORER, false); ArrayList files = intent.getParcelableArrayListExtra(TAG_COPY_SOURCES); String targetPath = intent.getStringExtra(TAG_COPY_TARGET); int mode = intent.getIntExtra(TAG_COPY_OPEN_MODE, OpenMode.UNKNOWN.ordinal()); final boolean move = intent.getBooleanExtra(TAG_COPY_MOVE, false); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(c); - accentColor = + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(c); + int accentColor = ((AppConfig) getApplication()) .getUtilsProvider() .getColorPreference() @@ -238,7 +232,7 @@ private class DoInBackground extends AsyncTask { boolean move; private Copy copy; private String targetPath; - private boolean isRootExplorer; + private final boolean isRootExplorer; private int sourceProgress = 0; private DoInBackground(boolean isRootExplorer) { @@ -251,8 +245,8 @@ protected Void doInBackground(Bundle... p1) { // setting up service watchers and initial data packages // finding total size on background thread (this is necessary condition for SMB!) - totalSize = FileUtils.getTotalBytes(sourceFiles, c); - totalSourceFiles = sourceFiles.size(); + long totalSize = FileUtils.getTotalBytes(sourceFiles, c); + int totalSourceFiles = sourceFiles.size(); progressHandler.setSourceSize(totalSourceFiles); progressHandler.setTotalSize(totalSize); @@ -435,7 +429,7 @@ public void execute( } } } else { - for (HybridFileParcelable f : sourceFiles) failedFOps.add(f); + failedFOps.addAll(sourceFiles); return; } @@ -455,7 +449,7 @@ void copyRoot(HybridFileParcelable sourceFile, HybridFile targetFile, boolean mo try { if (!move) { CopyFilesCommand.INSTANCE.copyFiles(sourceFile.getPath(), targetFile.getPath()); - } else if (move) { + } else { MoveFileCommand.INSTANCE.moveFile(sourceFile.getPath(), targetFile.getPath()); } ServiceWatcherUtil.position += sourceFile.getSize(); @@ -532,7 +526,7 @@ private void copyFiles( } } - private BroadcastReceiver receiver3 = + private final BroadcastReceiver receiver3 = new BroadcastReceiver() { @Override @@ -544,7 +538,6 @@ public void onReceive(Context context, Intent intent) { @Override public IBinder onBind(Intent arg0) { - // TODO Auto-generated method stub return mBinder; } } From e7cf56aad41f3dabf9b571503f59e3381cf84919 Mon Sep 17 00:00:00 2001 From: Obolrom Date: Sat, 1 Jul 2023 21:01:25 +0300 Subject: [PATCH 139/384] DecryptService minor code improvements --- .../filemanager/asynchronous/services/DecryptService.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/DecryptService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/DecryptService.java index c05d326b7b..e392a607e9 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/DecryptService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/DecryptService.java @@ -34,7 +34,6 @@ import com.amaze.filemanager.asynchronous.asynctasks.Task; import com.amaze.filemanager.asynchronous.asynctasks.TaskKt; import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.filesystem.FileProperties; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; @@ -113,8 +112,6 @@ public int onStartCommand(Intent intent, int flags, int startId) { .getCurrentUserColorPreferences(this, sharedPreferences) .getAccent(); - OpenMode openMode = - OpenMode.values()[intent.getIntExtra(TAG_OPEN_MODE, OpenMode.UNKNOWN.ordinal())]; notificationManager = NotificationManagerCompat.from(getApplicationContext()); Intent notificationIntent = new Intent(this, MainActivity.class); notificationIntent.setAction(Intent.ACTION_MAIN); @@ -267,7 +264,7 @@ public Callable getTask() { // and the cache directory in case we're here because of the viewer try { new CryptUtil(context, baseFile, decryptPath, progressHandler, failedOps, password); - } catch (AESCrypt.DecryptFailureException e) { + } catch (AESCrypt.DecryptFailureException ignored) { } catch (Exception e) { LOG.error("Error decrypting " + baseFile.getPath(), e); @@ -295,7 +292,7 @@ public void onDestroy() { this.unregisterReceiver(cancelReceiver); } - private BroadcastReceiver cancelReceiver = + private final BroadcastReceiver cancelReceiver = new BroadcastReceiver() { @Override From 846684444b0723ad168382dbfd723166590c9443 Mon Sep 17 00:00:00 2001 From: Obolrom Date: Sat, 1 Jul 2023 21:02:43 +0300 Subject: [PATCH 140/384] EncryptService minor code improvements --- .../asynchronous/services/EncryptService.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/EncryptService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/EncryptService.java index 897b64a109..28e1a1e128 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/EncryptService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/EncryptService.java @@ -33,7 +33,6 @@ import com.amaze.filemanager.asynchronous.asynctasks.Task; import com.amaze.filemanager.asynchronous.asynctasks.TaskKt; import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.filesystem.FileProperties; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; @@ -77,20 +76,18 @@ public class EncryptService extends AbstractProgressiveService { private NotificationManagerCompat notificationManager; private NotificationCompat.Builder notificationBuilder; private Context context; - private IBinder mBinder = new ObtainableServiceBinder<>(this); - private ProgressHandler progressHandler = new ProgressHandler(); + private final IBinder binder = new ObtainableServiceBinder<>(this); + private final ProgressHandler progressHandler = new ProgressHandler(); private ProgressListener progressListener; // list of data packages, to initiate chart in process viewer fragment - private ArrayList dataPackages = new ArrayList<>(); + private final ArrayList dataPackages = new ArrayList<>(); private ServiceWatcherUtil serviceWatcherUtil; private long totalSize = 0L; private HybridFileParcelable baseFile; - private ArrayList failedOps = new ArrayList<>(); + private final ArrayList failedOps = new ArrayList<>(); private String targetFilename; - private int accentColor; private boolean useAesCrypt; private String password; - private SharedPreferences sharedPreferences; private RemoteViews customSmallContentViews, customBigContentViews; @Override @@ -110,16 +107,14 @@ public int onStartCommand(Intent intent, int flags, int startId) { if (useAesCrypt) { password = intent.getStringExtra(TAG_PASSWORD); } - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - accentColor = + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + int accentColor = ((AppConfig) getApplication()) .getUtilsProvider() .getColorPreference() .getCurrentUserColorPreferences(this, sharedPreferences) .getAccent(); - OpenMode openMode = - OpenMode.values()[intent.getIntExtra(TAG_OPEN_MODE, OpenMode.UNKNOWN.ordinal())]; notificationManager = NotificationManagerCompat.from(getApplicationContext()); Intent notificationIntent = new Intent(this, MainActivity.class); notificationIntent.setAction(Intent.ACTION_MAIN); @@ -284,7 +279,7 @@ public Callable getTask() { @Override public IBinder onBind(Intent intent) { - return mBinder; + return binder; } @Override @@ -293,7 +288,7 @@ public void onDestroy() { this.unregisterReceiver(cancelReceiver); } - private BroadcastReceiver cancelReceiver = + private final BroadcastReceiver cancelReceiver = new BroadcastReceiver() { @Override From 28e5b73fea9983dd2e2982d0d7adc587be6e4820 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Fri, 7 Jul 2023 12:07:52 +0530 Subject: [PATCH 141/384] Minor changes in smb search dialog --- .../java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt index 014f5b5d3b..0b07aae815 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt @@ -96,7 +96,7 @@ class SmbSearchDialog : DialogFragment() { } builder.positiveText(R.string.use_custom_ip) builder.positiveColor(accentColor) - viewModel.valHolder.value = (ComputerParcelable("-1", "-1")) + viewModel.valHolder.postValue(ComputerParcelable("-1", "-1")) listViewAdapter = ListViewAdapter(requireActivity()) val observable = SmbDeviceScannerObservable() subnetScannerObserver = observable @@ -108,7 +108,7 @@ class SmbSearchDialog : DialogFragment() { .subscribe( { computer: ComputerParcelable -> if (!listViewAdapter.contains(computer)) { - viewModel.valHolder.value = computer + viewModel.valHolder.postValue(computer) } }, { err: Throwable -> @@ -156,7 +156,6 @@ class SmbSearchDialog : DialogFragment() { items.add(computer) } else { items.add(items.size - 1, computer) - removeDummy() } notifyDataSetChanged() } From 83f618411de094eae633cab1f01ee35b8b2629c5 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sun, 9 Jul 2023 12:33:51 +0800 Subject: [PATCH 142/384] Migrate removed Volley dependency to OkHttp To fix missing Volley dependency caused by #3872 when #3704 was still depends on Volley --- app/build.gradle | 3 + .../utils/smb/WsddDiscoverDeviceStrategy.kt | 61 ++++++++----------- build.gradle | 1 + .../filemanager/test/volley/MockHttpStack.kt | 51 ---------------- 4 files changed, 31 insertions(+), 85 deletions(-) delete mode 100644 testShared/src/test/java/com/amaze/filemanager/test/volley/MockHttpStack.kt diff --git a/app/build.gradle b/app/build.gradle index eeafa75fb6..6d4e53e4a4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -204,6 +204,9 @@ dependencies { //FTP implementation "commons-net:commons-net:$commonsNetVersion" + //OkHttp + implementation "com.squareup.okhttp3:okhttp:$okHttpVersion" + implementation "org.bouncycastle:bcpkix-jdk15on:$bouncyCastleVersion" implementation "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" diff --git a/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt index 33e1813c69..db5a1ab86f 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt @@ -23,12 +23,14 @@ package com.amaze.filemanager.utils.smb import androidx.annotation.VisibleForTesting import com.amaze.filemanager.R import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.SLASH import com.amaze.filemanager.utils.ComputerParcelable import com.amaze.filemanager.utils.NetworkUtil -import com.android.volley.Response.ErrorListener -import com.android.volley.VolleyError -import com.android.volley.toolbox.StringRequest -import com.android.volley.toolbox.Volley +import okhttp3.Headers.Companion.toHeaders +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody import org.slf4j.Logger import org.slf4j.LoggerFactory import org.xmlpull.v1.XmlPullParser @@ -75,7 +77,6 @@ class WsddDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDeviceStra private val wsdRequestHeaders = mutableMapOf( Pair("Accept-Encoding", "Identity"), - Pair("Content-Type", "application/soap+xml"), Pair("Connection", "Close"), Pair("User-Agent", "wsd") ) @@ -87,14 +88,10 @@ class WsddDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDeviceStra @VisibleForTesting set - private val queue = Volley.newRequestQueue(AppConfig.getInstance()) + private val queue = OkHttpClient() private var cancelled = false - init { - queue.start() - } - override fun discoverDevices(callback: (ComputerParcelable) -> Unit) { multicastForDevice { addr -> callback.invoke(addr) @@ -181,21 +178,30 @@ class WsddDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDeviceStra val endpoint = urn.substringAfter(URN_UUID) val dest = "http://${sourceAddress.hostAddress}:$TCP_PORT/$endpoint" - queue.add( - object : StringRequest( - Method.POST, - dest, - { resp -> + val requestBody = wsdRequestTemplate + .replace("##MESSAGE_ID##", "$URN_UUID$messageId") + .replace("##DEST_UUID##", urn) + .replace("##MY_UUID##", "$URN_UUID$tempDeviceId") + .toRequestBody("application/soap+xml".toMediaType()) + queue.newCall( + Request.Builder() + .url(dest) + .post(requestBody) + .headers(wsdRequestHeaders.toHeaders()) + .build() + ).execute().use { resp -> + if (resp.isSuccessful && resp.body != null) { + resp.body?.run { if (log.isTraceEnabled) log.trace("Response: $resp") val values = parseXmlForResponse( - resp, + this.string(), arrayOf(WSDP_TYPES, WSA_ADDRESS, PUB_COMPUTER) ) if (PUB_COMPUTER == values[WSDP_TYPES] && urn == values[WSA_ADDRESS]) { if (true == values[PUB_COMPUTER]?.isNotEmpty()) { val computerName: String = values[PUB_COMPUTER].let { - if (it!!.contains('/')) { - it.substringBefore("/") + if (it!!.contains(SLASH)) { + it.substringBefore(SLASH) } else { it } @@ -205,29 +211,16 @@ class WsddDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDeviceStra ) } } - }, - object : ErrorListener { - override fun onErrorResponse(error: VolleyError?) { - log.error("Error querying endpoint", error) - } } - ) { - override fun getBody(): ByteArray { - return wsdRequestTemplate - .replace("##MESSAGE_ID##", "$URN_UUID$messageId") - .replace("##DEST_UUID##", urn) - .replace("##MY_UUID##", "$URN_UUID$tempDeviceId") - .toByteArray(Charsets.UTF_8) - } - override fun getHeaders(): MutableMap = wsdRequestHeaders + } else { + log.error("Error querying endpoint", resp) } - ) + } } } override fun onCancel() { cancelled = true - queue.stop() } private fun parseXmlForResponse(xml: ByteArray, tags: Array) = diff --git a/build.gradle b/build.gradle index 3fd0ef3566..3d018b2808 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,7 @@ buildscript { jsoupVersion = "1.13.1" rxAndroidVersion = "2.1.1" rxJavaVersion = "2.2.9" + okHttpVersion = "4.9.0" } repositories { google() diff --git a/testShared/src/test/java/com/amaze/filemanager/test/volley/MockHttpStack.kt b/testShared/src/test/java/com/amaze/filemanager/test/volley/MockHttpStack.kt deleted file mode 100644 index f859a35b43..0000000000 --- a/testShared/src/test/java/com/amaze/filemanager/test/volley/MockHttpStack.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.amaze.filemanager.test.volley - -import com.android.volley.AuthFailureError -import com.android.volley.Request -import com.android.volley.toolbox.BaseHttpStack -import com.android.volley.toolbox.HttpResponse -import java.io.IOException - -/** - * Mock [BaseHttpStack] for test only. - */ -class MockHttpStack : BaseHttpStack() { - - private lateinit var mResponseToReturn: HttpResponse - private lateinit var lastUrl: String - private lateinit var mLastHeaders: MutableMap - private var lastPostBody: ByteArray? = null - - /** - * get headers in last request - */ - fun getLastHeaders() = mLastHeaders - - /** - * Manually set response to return - */ - fun setResponseToReturn(response: HttpResponse) { - mResponseToReturn = response - } - - @Throws(IOException::class, AuthFailureError::class) - override fun executeRequest( - request: Request<*>, - additionalHeaders: Map? - ): HttpResponse { - lastUrl = request.url - mLastHeaders = HashMap() - if (request.headers != null) { - mLastHeaders.putAll(request.headers) - } - if (additionalHeaders != null) { - mLastHeaders.putAll(additionalHeaders) - } - try { - lastPostBody = request.body - } catch (e: AuthFailureError) { - lastPostBody = null - } - return mResponseToReturn - } -} From 356068d69786630313083bfc9ec8f10f95680843 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 18 Feb 2023 11:40:45 +0800 Subject: [PATCH 143/384] SDK 33 adaptations + lib upgrades It has to happen anyway. - Compile with SDK 33 - Use Kotlin 1.8 - Use Java 11 - Library upgrades - Fix syntax and API problems brought by the upgrade --- app/build.gradle | 19 +++++++++--------- .../ui/base/BaseBottomSheetFragment.kt | 4 ++-- .../ui/views/drawer/HasherOfMenuItem.kt | 2 +- .../database/MigrationTestHelper.java | 14 ++++++------- build.gradle | 20 +++++++++---------- commons_compress_7z/build.gradle | 2 +- file_operations/build.gradle | 4 ++-- 7 files changed, 32 insertions(+), 33 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index eeafa75fb6..204cd68677 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'com.hiya.jacoco-android' apply plugin: "com.starter.easylauncher" android { - compileSdkVersion 31 + compileSdkVersion 33 packagingOptions { resources { excludes += ['proguard-project.txt', 'project.properties', 'META-INF/LICENSE.txt', 'META-INF/LICENSE', 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/DEPENDENCIES.txt', 'META-INF/DEPENDENCIES'] @@ -17,7 +17,7 @@ android { defaultConfig { applicationId "com.amaze.filemanager" minSdkVersion 19 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 118 versionName "3.8.5" multiDexEnabled true @@ -85,13 +85,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = '1.8' - useIR = true + jvmTarget = '11' } testOptions { @@ -252,11 +251,11 @@ dependencies { configurations.all { resolutionStrategy { dependencySubstitution { - substitute module("commons-logging:commons-logging-api:1.1") with module("commons-logging:commons-logging:1.1.1") - substitute module("com.android.support:support-annotations:27.1.1") with module("com.android.support:support-annotations:27.0.2") + substitute module("commons-logging:commons-logging-api:1.1") using module("commons-logging:commons-logging:1.1.1") + substitute module("com.android.support:support-annotations:27.1.1") using module("com.android.support:support-annotations:27.0.2") // These two lines are added to prevent possible class clashes between awaitility (which uses hamcrest 2.1) and junit (which uses hamcrest 1.3). - substitute module('org.hamcrest:hamcrest-core:1.3') with module("org.hamcrest:hamcrest:2.1") - substitute module('org.hamcrest:hamcrest-library:1.3') with module("org.hamcrest:hamcrest:2.1") + substitute module('org.hamcrest:hamcrest-core:1.3') using module("org.hamcrest:hamcrest:2.1") + substitute module('org.hamcrest:hamcrest-library:1.3') using module("org.hamcrest:hamcrest:2.1") } } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/base/BaseBottomSheetFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/base/BaseBottomSheetFragment.kt index 78c3b6449f..c8e6551fa8 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/base/BaseBottomSheetFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/base/BaseBottomSheetFragment.kt @@ -39,7 +39,7 @@ open class BaseBottomSheetFragment : BottomSheetDialogFragment() { * Initializes bottom sheet ui resources based on current theme */ fun initDialogResources(rootView: View) { - when ((activity as ThemedActivity?)!!.appTheme!!) { + when ((requireActivity() as ThemedActivity).appTheme!!) { AppTheme.DARK -> { rootView.setBackgroundDrawable( context?.resources?.getDrawable( @@ -54,7 +54,7 @@ open class BaseBottomSheetFragment : BottomSheetDialogFragment() { ) ) } - AppTheme.LIGHT, AppTheme.TIMED -> { + AppTheme.LIGHT, AppTheme.TIMED, AppTheme.SYSTEM -> { rootView .setBackgroundDrawable( context?.resources?.getDrawable( diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/HasherOfMenuItem.kt b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/HasherOfMenuItem.kt index 1e09c91473..bbf04494d1 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/HasherOfMenuItem.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/HasherOfMenuItem.kt @@ -28,7 +28,7 @@ import android.view.MenuItem data class HasherOfMenuItem( val groupId: Int, val itemId: Int, - val title: CharSequence, + val title: CharSequence?, val ordering: Int ) diff --git a/app/src/test/java/com/amaze/filemanager/database/MigrationTestHelper.java b/app/src/test/java/com/amaze/filemanager/database/MigrationTestHelper.java index d5dcacc0c7..e5094cfa30 100644 --- a/app/src/test/java/com/amaze/filemanager/database/MigrationTestHelper.java +++ b/app/src/test/java/com/amaze/filemanager/database/MigrationTestHelper.java @@ -477,7 +477,7 @@ static class MigratingDelegate extends MigrationTestHelper.RoomOpenHelperDelegat } @Override - protected void createAllTables(SupportSQLiteDatabase database) { + public void createAllTables(SupportSQLiteDatabase database) { throw new UnsupportedOperationException( "Was expecting to migrate but received create." + "Make sure you have created the database first."); @@ -485,7 +485,7 @@ protected void createAllTables(SupportSQLiteDatabase database) { @NonNull @Override - protected RoomOpenHelper.ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) { + public RoomOpenHelper.ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) { final Map tables = mDatabaseBundle.getEntitiesByTableName(); for (EntityBundle entity : tables.values()) { if (entity instanceof FtsEntityBundle) { @@ -548,7 +548,7 @@ static class CreatingDelegate extends MigrationTestHelper.RoomOpenHelperDelegate } @Override - protected void createAllTables(SupportSQLiteDatabase database) { + public void createAllTables(SupportSQLiteDatabase database) { for (String query : mDatabaseBundle.buildCreateQueries()) { database.execSQL(query); } @@ -556,7 +556,7 @@ protected void createAllTables(SupportSQLiteDatabase database) { @NonNull @Override - protected RoomOpenHelper.ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) { + public RoomOpenHelper.ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) { throw new UnsupportedOperationException( "This open helper just creates the database but" + " it received a migration request."); } @@ -571,14 +571,14 @@ abstract static class RoomOpenHelperDelegate extends RoomOpenHelper.Delegate { } @Override - protected void dropAllTables(SupportSQLiteDatabase database) { + public void dropAllTables(SupportSQLiteDatabase database) { throw new UnsupportedOperationException("cannot drop all tables in the test"); } @Override - protected void onCreate(SupportSQLiteDatabase database) {} + public void onCreate(SupportSQLiteDatabase database) {} @Override - protected void onOpen(SupportSQLiteDatabase database) {} + public void onOpen(SupportSQLiteDatabase database) {} } } diff --git a/build.gradle b/build.gradle index 3fd0ef3566..1d9736c176 100644 --- a/build.gradle +++ b/build.gradle @@ -2,23 +2,23 @@ buildscript { ext { - kotlin_version = "1.6.10" + kotlin_version = "1.8.0" robolectricVersion = '4.9' glideVersion = '4.11.0' sshjVersion = '0.34.0' jcifsVersion = '2.1.6' fabSpeedDialVersion = '3.1.1' - roomVersion = '2.4.3' + roomVersion = '2.5.0' bouncyCastleVersion = '1.70' awaitilityVersion = "3.1.6" androidXCoreVersion = "1.7.0" - androidMaterialVersion = "1.4.0" // Upgrade to 1.5 requires targetSdkVersion 31 - androidXFragmentVersion = "1.4.1" - androidXAppCompatVersion = "1.4.1" - androidXAnnotationVersion = "1.3.0" + androidMaterialVersion = "1.5.0" // Upgrade to 1.5 requires targetSdkVersion 31 + androidXFragmentVersion = "1.5.5" + androidXAppCompatVersion = "1.6.1" + androidXAnnotationVersion = "1.5.0" androidXPrefVersion = "1.2.0" - androidXTestVersion = "1.4.0" - androidXTestExtVersion = "1.1.3" + androidXTestVersion = "1.5.0" + androidXTestExtVersion = "1.1.5" uiAutomatorVersion = "2.2.0" junitVersion = "4.13.2" slf4jVersion = "1.7.25" @@ -27,7 +27,7 @@ buildscript { androidBillingVersion = "5.0.0" junrarVersion = "7.4.0" zip4jVersion = "2.6.4" - espressoVersion = "3.4.0" + espressoVersion = "3.5.1" materialDialogsVersion = "0.9.6.0" jacocoVersion = "0.8.7" commonsCompressVersion = "1.22" @@ -68,7 +68,7 @@ allprojects { maven { url "https://jcenter.bintray.com" } } tasks.withType(Test) { - maxParallelForks = 4 + maxParallelForks = 8 maxHeapSize = "2g" forkEvery = 4 failFast = true diff --git a/commons_compress_7z/build.gradle b/commons_compress_7z/build.gradle index 0b7e3a9e9a..428e80db73 100644 --- a/commons_compress_7z/build.gradle +++ b/commons_compress_7z/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion 33 defaultConfig { minSdkVersion 19 diff --git a/file_operations/build.gradle b/file_operations/build.gradle index eadddd661e..47cefa5e63 100644 --- a/file_operations/build.gradle +++ b/file_operations/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 33 defaultConfig { minSdkVersion 19 @@ -51,7 +51,7 @@ android { } kotlinOptions { - useIR = true + jvmTarget = '11' } } From 43d148f5380bb6c2d783757bd466932e36265159 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Thu, 20 Jul 2023 00:17:07 +0800 Subject: [PATCH 144/384] Changes per feedback Add POST_NOTIFICATION permission request --- app/src/main/AndroidManifest.xml | 1 + .../ui/activities/MainActivity.java | 7 +++- .../superclasses/PermissionsActivity.java | 38 +++++++++++++++++-- app/src/main/res/values/strings.xml | 2 + 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 96ff20b8a3..5bd6da5ace 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,7 @@ + = Build.VERSION_CODES.M) { + if (SDK_INT >= Build.VERSION_CODES.M) { if (!checkStoragePermission()) { requestStoragePermission(this, true); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (SDK_INT >= Build.VERSION_CODES.R) { requestAllFilesAccess(this); } + if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestNotificationPermission(true); + } } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java index 9c97929871..c0bd3a31a9 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java @@ -20,6 +20,8 @@ package com.amaze.filemanager.ui.activities.superclasses; +import static android.os.Build.VERSION_CODES.TIRAMISU; + import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; @@ -48,10 +50,11 @@ public class PermissionsActivity extends ThemedActivity private static final String TAG = PermissionsActivity.class.getSimpleName(); - public static final int PERMISSION_LENGTH = 3; + public static final int PERMISSION_LENGTH = 4; public static final int STORAGE_PERMISSION = 0, INSTALL_APK_PERMISSION = 1, - ALL_FILES_PERMISSION = 2; + ALL_FILES_PERMISSION = 2, + NOTIFICATION_PERMISSION = 3; private final OnPermissionGranted[] permissionCallbacks = new OnPermissionGranted[PERMISSION_LENGTH]; @@ -69,7 +72,13 @@ public void onRequestPermissionsResult( Toast.makeText(this, R.string.grantfailed, Toast.LENGTH_SHORT).show(); requestStoragePermission(permissionCallbacks[STORAGE_PERMISSION], false); } - + } else if (requestCode == NOTIFICATION_PERMISSION && Build.VERSION.SDK_INT >= TIRAMISU) { + if (isGranted(grantResults)) { + Utils.enableScreenRotation(this); + } else { + Toast.makeText(this, R.string.grantfailed, Toast.LENGTH_SHORT).show(); + requestNotificationPermission(false); + } } else if (requestCode == INSTALL_APK_PERMISSION) { if (isGranted(grantResults)) { permissionCallbacks[INSTALL_APK_PERMISSION].onPermissionGranted(); @@ -84,6 +93,29 @@ public boolean checkStoragePermission() { == PackageManager.PERMISSION_GRANTED; } + @RequiresApi(api = TIRAMISU) + public void requestNotificationPermission(boolean isInitialStart) { + Utils.disableScreenRotation(this); + final MaterialDialog materialDialog = + GeneralDialogCreation.showBasicDialog( + this, + R.string.grant_notification_permission, + R.string.grantper, + R.string.grant, + R.string.cancel); + materialDialog.getActionButton(DialogAction.NEGATIVE).setOnClickListener(v -> finish()); + materialDialog.setCancelable(false); + + requestPermission( + Manifest.permission.POST_NOTIFICATIONS, + NOTIFICATION_PERMISSION, + materialDialog, + () -> { + //do nothing + }, + isInitialStart); + } + public void requestStoragePermission( @NonNull final OnPermissionGranted onPermissionGranted, boolean isInitialStart) { Utils.disableScreenRotation(this); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fc0ca66fc..8bec20b847 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -813,5 +813,7 @@ You only need to do this once, until the next time you select a new location for Try Indexed Search! Recent Results + + Amaze needs notification permission to display file operation progress, as well as a handy panel to start/stop the FTP server. From b71b711551234ffae824a8d11af9cfb488e53913 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Thu, 20 Jul 2023 00:17:07 +0800 Subject: [PATCH 145/384] Changes per feedback - Add POST_NOTIFICATION permission request - Add additional permissions required by API 33 for storage access --- .../ftp/FtpServiceStaticMethodsTest.kt | 5 ++- .../security/SecretKeygenEspressoTest.kt | 2 + app/src/main/AndroidManifest.xml | 5 +++ .../ui/activities/MainActivity.java | 7 ++- .../superclasses/PermissionsActivity.java | 44 +++++++++++++++++-- app/src/main/res/values/strings.xml | 2 + 6 files changed, 58 insertions(+), 7 deletions(-) diff --git a/app/src/androidTest/java/com/amaze/filemanager/asynchronous/services/ftp/FtpServiceStaticMethodsTest.kt b/app/src/androidTest/java/com/amaze/filemanager/asynchronous/services/ftp/FtpServiceStaticMethodsTest.kt index 830e55fe91..afa89840a9 100644 --- a/app/src/androidTest/java/com/amaze/filemanager/asynchronous/services/ftp/FtpServiceStaticMethodsTest.kt +++ b/app/src/androidTest/java/com/amaze/filemanager/asynchronous/services/ftp/FtpServiceStaticMethodsTest.kt @@ -26,6 +26,7 @@ import android.os.Build.VERSION_CODES.N_MR1 import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.amaze.filemanager.utils.NetworkUtil import org.junit.Assert.assertNotNull import org.junit.Assert.fail import org.junit.Test @@ -53,11 +54,11 @@ class FtpServiceStaticMethodsTest { */ if (SDK_INT >= N_MR1) { ApplicationProvider.getApplicationContext().run { - if (!FtpService.isConnectedToLocalNetwork(this)) { + if (!NetworkUtil.isConnectedToLocalNetwork(this)) { fail("Please connect your device to network to run this test!") } - FtpService.getLocalInetAddress(this).also { + NetworkUtil.getLocalInetAddress(this).also { assertNotNull(it) assertNotNull(it?.hostAddress) } diff --git a/app/src/androidTest/java/com/amaze/filemanager/utils/security/SecretKeygenEspressoTest.kt b/app/src/androidTest/java/com/amaze/filemanager/utils/security/SecretKeygenEspressoTest.kt index 773113c169..293accbf3e 100644 --- a/app/src/androidTest/java/com/amaze/filemanager/utils/security/SecretKeygenEspressoTest.kt +++ b/app/src/androidTest/java/com/amaze/filemanager/utils/security/SecretKeygenEspressoTest.kt @@ -51,6 +51,8 @@ class SecretKeygenEspressoTest { assertEquals("aes", this.algorithm.lowercase()) } ?: if (SDK_INT < ICE_CREAM_SANDWICH) { fail("Android version not supported") + } else { + // do nothing } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 96ff20b8a3..92c271bee6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,7 @@ + @@ -39,6 +40,10 @@ + + + + = Build.VERSION_CODES.M) { + if (SDK_INT >= Build.VERSION_CODES.M) { if (!checkStoragePermission()) { requestStoragePermission(this, true); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (SDK_INT >= Build.VERSION_CODES.R) { requestAllFilesAccess(this); } + if (SDK_INT >= Build.VERSION_CODES.TIRAMISU && !checkNotificationPermission()) { + requestNotificationPermission(true); + } } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java index 9c97929871..7204cee67c 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java @@ -20,6 +20,8 @@ package com.amaze.filemanager.ui.activities.superclasses; +import static android.os.Build.VERSION_CODES.TIRAMISU; + import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; @@ -48,10 +50,11 @@ public class PermissionsActivity extends ThemedActivity private static final String TAG = PermissionsActivity.class.getSimpleName(); - public static final int PERMISSION_LENGTH = 3; + public static final int PERMISSION_LENGTH = 4; public static final int STORAGE_PERMISSION = 0, INSTALL_APK_PERMISSION = 1, - ALL_FILES_PERMISSION = 2; + ALL_FILES_PERMISSION = 2, + NOTIFICATION_PERMISSION = 3; private final OnPermissionGranted[] permissionCallbacks = new OnPermissionGranted[PERMISSION_LENGTH]; @@ -69,7 +72,13 @@ public void onRequestPermissionsResult( Toast.makeText(this, R.string.grantfailed, Toast.LENGTH_SHORT).show(); requestStoragePermission(permissionCallbacks[STORAGE_PERMISSION], false); } - + } else if (requestCode == NOTIFICATION_PERMISSION && Build.VERSION.SDK_INT >= TIRAMISU) { + if (isGranted(grantResults)) { + Utils.enableScreenRotation(this); + } else { + Toast.makeText(this, R.string.grantfailed, Toast.LENGTH_SHORT).show(); + requestNotificationPermission(false); + } } else if (requestCode == INSTALL_APK_PERMISSION) { if (isGranted(grantResults)) { permissionCallbacks[INSTALL_APK_PERMISSION].onPermissionGranted(); @@ -84,6 +93,35 @@ public boolean checkStoragePermission() { == PackageManager.PERMISSION_GRANTED; } + @RequiresApi(TIRAMISU) + public boolean checkNotificationPermission() { + return ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + == PackageManager.PERMISSION_GRANTED; + } + + @RequiresApi(TIRAMISU) + public void requestNotificationPermission(boolean isInitialStart) { + Utils.disableScreenRotation(this); + final MaterialDialog materialDialog = + GeneralDialogCreation.showBasicDialog( + this, + R.string.grant_notification_permission, + R.string.grantper, + R.string.grant, + R.string.cancel); + materialDialog.getActionButton(DialogAction.NEGATIVE).setOnClickListener(v -> finish()); + materialDialog.setCancelable(false); + + requestPermission( + Manifest.permission.POST_NOTIFICATIONS, + NOTIFICATION_PERMISSION, + materialDialog, + () -> { + // do nothing + }, + isInitialStart); + } + public void requestStoragePermission( @NonNull final OnPermissionGranted onPermissionGranted, boolean isInitialStart) { Utils.disableScreenRotation(this); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fc0ca66fc..8bec20b847 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -813,5 +813,7 @@ You only need to do this once, until the next time you select a new location for Try Indexed Search! Recent Results + + Amaze needs notification permission to display file operation progress, as well as a handy panel to start/stop the FTP server. From 09fdaf3c3ee31bb931f5dd70100c1e56ae9289c3 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Fri, 21 Jul 2023 03:51:42 +0530 Subject: [PATCH 146/384] Modifications in asking storage permission in T --- .../ui/activities/MainActivity.java | 9 ++- .../superclasses/PermissionsActivity.java | 73 ++++++++++++------- 2 files changed, 53 insertions(+), 29 deletions(-) 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 455e88e98a..23c2eeb3e3 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 @@ -557,10 +557,11 @@ public void onPermissionGranted() { private void checkForExternalPermission() { if (SDK_INT >= Build.VERSION_CODES.M) { if (!checkStoragePermission()) { - requestStoragePermission(this, true); - } - if (SDK_INT >= Build.VERSION_CODES.R) { - requestAllFilesAccess(this); + if (SDK_INT >= Build.VERSION_CODES.R) { + requestAllFilesAccess(this); + } else { + requestStoragePermission(this, true); + } } if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { requestNotificationPermission(true); diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java index c0bd3a31a9..d8b374d3a9 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java @@ -20,12 +20,14 @@ package com.amaze.filemanager.ui.activities.superclasses; +import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.TIRAMISU; import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; import com.amaze.filemanager.application.AppConfig; +import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation; import com.amaze.filemanager.utils.Utils; import com.google.android.material.snackbar.BaseTransientBottomBar; @@ -89,8 +91,13 @@ public void onRequestPermissionsResult( public boolean checkStoragePermission() { // Verify that all required contact permissions have been granted. - return ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED; + if (SDK_INT >= Build.VERSION_CODES.R) { + return ActivityCompat.checkSelfPermission(this, Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + == PackageManager.PERMISSION_GRANTED; + } else { + return ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } } @RequiresApi(api = TIRAMISU) @@ -192,18 +199,29 @@ private void requestPermission( } else if (isInitialStart) { ActivityCompat.requestPermissions(this, new String[] {permission}, code); } else { - Snackbar.make( - findViewById(R.id.content_frame), - R.string.grantfailed, - BaseTransientBottomBar.LENGTH_INDEFINITE) - .setAction( - R.string.grant, - v -> - startActivity( - new Intent( - android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse(String.format("package:%s", getPackageName()))))) - .show(); + if (SDK_INT >= Build.VERSION_CODES.R) { + Snackbar.make( + findViewById(R.id.content_frame), + R.string.grantfailed, + BaseTransientBottomBar.LENGTH_INDEFINITE) + .setAction( + R.string.grant, + v -> requestAllFilesAccessPermission(onPermissionGranted)) + .show(); + } else { + Snackbar.make( + findViewById(R.id.content_frame), + R.string.grantfailed, + BaseTransientBottomBar.LENGTH_INDEFINITE) + .setAction( + R.string.grant, + v -> + startActivity( + new Intent( + android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse(String.format("package:%s", getPackageName()))))) + .show(); + } } } @@ -226,17 +244,7 @@ public void requestAllFilesAccess(@NonNull final OnPermissionGranted onPermissio .getActionButton(DialogAction.POSITIVE) .setOnClickListener( v -> { - Utils.disableScreenRotation(this); - permissionCallbacks[ALL_FILES_PERMISSION] = onPermissionGranted; - try { - Intent intent = - new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - .setData(Uri.parse("package:" + getPackageName())); - startActivity(intent); - } catch (Exception e) { - Log.e(TAG, "Failed to initial activity to grant all files access", e); - AppConfig.toast(this, getString(R.string.grantfailed)); - } + requestAllFilesAccessPermission(onPermissionGranted); materialDialog.dismiss(); }); materialDialog.setCancelable(false); @@ -244,6 +252,21 @@ public void requestAllFilesAccess(@NonNull final OnPermissionGranted onPermissio } } + @RequiresApi(api = Build.VERSION_CODES.R) + private void requestAllFilesAccessPermission(@NonNull final OnPermissionGranted onPermissionGranted) { + Utils.disableScreenRotation(this); + permissionCallbacks[ALL_FILES_PERMISSION] = onPermissionGranted; + try { + Intent intent = + new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + .setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + } catch (Exception e) { + Log.e(TAG, "Failed to initial activity to grant all files access", e); + AppConfig.toast(this, getString(R.string.grantfailed)); + } + } + private boolean isGranted(int[] grantResults) { return grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED; } From 4432ea712df94ab25baf461e26d066a1a2b6582f Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Fri, 21 Jul 2023 23:30:25 +0800 Subject: [PATCH 147/384] Fix codacy and spotless complaint Also enable use of jvmToolchain for Kotlin related compilation stuff. See https://youtrack.jetbrains.com/issue/KTIJ-24311/task-current-target-is-17-and-kaptGenerateStubsProductionDebugKotlin-task-current-target-is-1.8-jvm-target-compatibility-should#focus=Comments-27-6798448.0-0 for reason of the workaround --- app/build.gradle | 5 ++- .../superclasses/PermissionsActivity.java | 45 ++++++++++--------- file_operations/build.gradle | 8 +++- portscanner/build.gradle | 12 +++-- 4 files changed, 41 insertions(+), 29 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9bd34bcebd..ee99efc317 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -251,6 +251,10 @@ dependencies { } +kotlin { + jvmToolchain(11) +} + configurations.all { resolutionStrategy { dependencySubstitution { @@ -305,7 +309,6 @@ tasks.withType(Test) { jacoco.excludes = ['jdk.internal.*'] } - Properties props = new Properties() def propFile = new File('signing.properties') diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java index eba49fc7d5..3ca97c186b 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/superclasses/PermissionsActivity.java @@ -20,8 +20,9 @@ package com.amaze.filemanager.ui.activities.superclasses; -import static android.os.Build.VERSION_CODES.TIRAMISU; import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.TIRAMISU; + import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; @@ -72,7 +73,7 @@ public void onRequestPermissionsResult( Toast.makeText(this, R.string.grantfailed, Toast.LENGTH_SHORT).show(); requestStoragePermission(permissionCallbacks[STORAGE_PERMISSION], false); } - } else if (requestCode == NOTIFICATION_PERMISSION && Build.VERSION.SDK_INT >= TIRAMISU) { + } else if (requestCode == NOTIFICATION_PERMISSION && SDK_INT >= TIRAMISU) { if (isGranted(grantResults)) { Utils.enableScreenRotation(this); } else { @@ -90,11 +91,12 @@ public void onRequestPermissionsResult( public boolean checkStoragePermission() { // Verify that all required contact permissions have been granted. if (SDK_INT >= Build.VERSION_CODES.R) { - return ActivityCompat.checkSelfPermission(this, Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - == PackageManager.PERMISSION_GRANTED; + return ActivityCompat.checkSelfPermission( + this, Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + == PackageManager.PERMISSION_GRANTED; } else { return ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED; + == PackageManager.PERMISSION_GRANTED; } } @@ -208,23 +210,21 @@ private void requestPermission( findViewById(R.id.content_frame), R.string.grantfailed, BaseTransientBottomBar.LENGTH_INDEFINITE) - .setAction( - R.string.grant, - v -> requestAllFilesAccessPermission(onPermissionGranted)) + .setAction(R.string.grant, v -> requestAllFilesAccessPermission(onPermissionGranted)) .show(); } else { Snackbar.make( - findViewById(R.id.content_frame), - R.string.grantfailed, - BaseTransientBottomBar.LENGTH_INDEFINITE) - .setAction( - R.string.grant, - v -> - startActivity( - new Intent( - android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse(String.format("package:%s", getPackageName()))))) - .show(); + findViewById(R.id.content_frame), + R.string.grantfailed, + BaseTransientBottomBar.LENGTH_INDEFINITE) + .setAction( + R.string.grant, + v -> + startActivity( + new Intent( + android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse(String.format("package:%s", getPackageName()))))) + .show(); } } } @@ -257,13 +257,14 @@ public void requestAllFilesAccess(@NonNull final OnPermissionGranted onPermissio } @RequiresApi(api = Build.VERSION_CODES.R) - private void requestAllFilesAccessPermission(@NonNull final OnPermissionGranted onPermissionGranted) { + private void requestAllFilesAccessPermission( + @NonNull final OnPermissionGranted onPermissionGranted) { Utils.disableScreenRotation(this); permissionCallbacks[ALL_FILES_PERMISSION] = onPermissionGranted; try { Intent intent = - new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - .setData(Uri.parse("package:" + getPackageName())); + new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + .setData(Uri.parse("package:" + getPackageName())); startActivity(intent); } catch (Exception e) { Log.e(TAG, "Failed to initial activity to grant all files access", e); diff --git a/file_operations/build.gradle b/file_operations/build.gradle index 47cefa5e63..1caa72ac33 100644 --- a/file_operations/build.gradle +++ b/file_operations/build.gradle @@ -32,8 +32,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } testOptions { @@ -55,6 +55,10 @@ android { } } +kotlin { + jvmToolchain(11) +} + dependencies { modules { module("org.bouncycastle:bcprov-jdk15to18") { diff --git a/portscanner/build.gradle b/portscanner/build.gradle index 355d17f1c3..65b6b3e2f0 100644 --- a/portscanner/build.gradle +++ b/portscanner/build.gradle @@ -5,11 +5,11 @@ apply plugin: 'kotlin-parcelize' android { namespace 'com.stealthcopter.networktools' - compileSdk 32 + compileSdk 33 defaultConfig { minSdk 14 - targetSdk 32 + targetSdk 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -22,11 +22,15 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } } +kotlin { + jvmToolchain(11) +} + dependencies { implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" // Because RxAndroid releases are few and far between, it is recommended you also From 2612b829e55fcf66b865d4251ed0b1dbe7351633 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 22 Jul 2023 16:00:36 +0800 Subject: [PATCH 148/384] Migrate widgets to use AppCompat versions where possible Fixes #3890 --- .../adapters/CompressedExplorerAdapter.java | 8 +- .../filemanager/adapters/RecyclerAdapter.java | 14 +- .../adapters/SearchRecyclerViewAdapter.kt | 6 +- .../glide/AppsAdapterPreloadModel.java | 4 +- .../filemanager/adapters/holders/AppHolder.kt | 18 +- .../holders/CompressedItemViewHolder.kt | 18 +- .../adapters/holders/DonationViewHolder.kt | 8 +- .../adapters/holders/HiddenViewHolder.kt | 10 +- .../adapters/holders/ItemViewHolder.kt | 28 +-- .../adapters/holders/SpecialViewHolder.kt | 4 +- .../asynctasks/CountItemsOrAndSizeTask.java | 6 +- .../hashcalculator/CalculateHashTask.kt | 8 +- .../asynctasks/movecopy/PrepareCopyTask.java | 4 +- .../asynchronous/services/ExtractService.java | 4 +- .../crashreport/ErrorActivity.java | 26 +-- .../filesystem/files/EncryptDecryptUtils.java | 4 +- .../com/amaze/filemanager/ui/Extensions.kt | 4 +- .../ui/activities/AboutActivity.java | 4 +- .../texteditor/TextEditorActivity.java | 14 +- .../ui/dialogs/ColorPickerDialog.java | 12 +- .../ui/dialogs/DecryptFingerprintDialog.kt | 6 +- .../ui/dialogs/DragAndDropDialog.kt | 10 +- .../ui/dialogs/GeneralDialogCreation.java | 66 +++--- .../ui/dialogs/SmbConnectDialog.java | 4 +- .../filemanager/ui/dialogs/SmbSearchDialog.kt | 10 +- .../ui/dialogs/share/ShareAdapter.java | 8 +- .../fragments/CompressedExplorerFragment.kt | 14 +- .../ui/fragments/DbViewerFragment.java | 4 +- .../ui/fragments/FtpServerFragment.kt | 22 +- .../ui/fragments/MainFragment.java | 20 +- .../filemanager/ui/fragments/TabFragment.java | 6 +- .../BookmarksPrefsFragment.kt | 7 +- .../ui/selection/SelectionPopupMenu.kt | 5 +- .../filemanager/ui/views/FastScroller.java | 4 +- .../filemanager/ui/views/ThemedTextView.java | 4 +- .../ui/views/WarnableTextInputValidator.java | 6 +- .../ui/views/appbar/BottomBar.java | 36 ++-- .../ui/views/appbar/SearchView.java | 14 +- .../views/drawer/ActionViewStateManager.java | 6 +- .../filemanager/ui/views/drawer/Drawer.java | 17 +- .../utils/MainActivityActionMode.kt | 5 +- .../filemanager/utils/MainActivityHelper.java | 19 +- .../com/amaze/filemanager/utils/Utils.java | 8 +- app/src/main/res/layout-v16/grid_header.xml | 4 +- app/src/main/res/layout-v16/list_header.xml | 4 +- app/src/main/res/layout-v21/layout_appbar.xml | 6 +- app/src/main/res/layout-v21/layout_search.xml | 8 +- .../res/layout-w500dp/properties_dialog.xml | 24 +-- .../main/res/layout-w720dp/layout_appbar.xml | 6 +- .../main/res/layout-w720dp/layout_search.xml | 8 +- .../main/res/layout-w720dp/main_toolbar.xml | 4 +- app/src/main/res/layout/actionmode.xml | 4 +- .../main/res/layout/actionmode_textviewer.xml | 10 +- app/src/main/res/layout/activity_about.xml | 194 +++++++++--------- app/src/main/res/layout/activity_error.xml | 30 +-- app/src/main/res/layout/bookmarkrow.xml | 5 +- app/src/main/res/layout/copy_dialog.xml | 6 +- ...log_decrypt_fingerprint_authentication.xml | 2 +- .../main/res/layout/dialog_twoedittexts.xml | 4 +- app/src/main/res/layout/drag_placeholder.xml | 4 +- app/src/main/res/layout/drawerheader.xml | 6 +- app/src/main/res/layout/fastscroller.xml | 13 +- app/src/main/res/layout/fragment_app_list.xml | 2 +- .../main/res/layout/fragment_db_viewer.xml | 2 +- app/src/main/res/layout/fragment_ftp.xml | 18 +- .../res/layout/fragment_open_file_dialog.xml | 12 +- .../main/res/layout/fragment_sheet_cloud.xml | 2 +- app/src/main/res/layout/grid_header.xml | 4 +- app/src/main/res/layout/griditem.xml | 9 +- app/src/main/res/layout/layout_appbar.xml | 6 +- .../res/layout/layout_draweractionview.xml | 2 +- app/src/main/res/layout/layout_search.xml | 6 +- app/src/main/res/layout/lexadrawer.xml | 8 +- app/src/main/res/layout/list_header.xml | 4 +- app/src/main/res/layout/main_frag.xml | 10 +- app/src/main/res/layout/main_toolbar.xml | 4 +- app/src/main/res/layout/permissiontable.xml | 32 +-- app/src/main/res/layout/processparent.xml | 9 +- app/src/main/res/layout/properties_audio.xml | 18 +- app/src/main/res/layout/properties_dialog.xml | 24 +-- .../main/res/layout/properties_document.xml | 62 +++--- .../main/res/layout/properties_general.xml | 30 +-- app/src/main/res/layout/properties_image.xml | 58 +++--- .../res/layout/properties_information.xml | 26 +-- app/src/main/res/layout/properties_video.xml | 18 +- app/src/main/res/layout/rowlayout.xml | 27 ++- app/src/main/res/layout/search.xml | 2 +- app/src/main/res/layout/search_row_item.xml | 4 +- app/src/main/res/layout/simplerow.xml | 4 +- app/src/main/res/layout/smb_computers_row.xml | 6 +- app/src/main/res/layout/smb_dialog.xml | 2 +- app/src/main/res/layout/snackbar_view.xml | 6 +- app/src/main/res/layout/tabfragment.xml | 4 +- .../res/layout/utilities_alias_layout.xml | 8 +- .../asynctasks/DbViewerTaskTest.java | 11 +- .../ssh/PemToKeyPairObservableRsaTest.kt | 4 +- .../ui/activities/TextEditorActivityTest.java | 4 +- .../views/WarnableTextInputValidatorTest.java | 6 +- 98 files changed, 660 insertions(+), 641 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/CompressedExplorerAdapter.java b/app/src/main/java/com/amaze/filemanager/adapters/CompressedExplorerAdapter.java index 3917d23fb9..73b6e2bb07 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/CompressedExplorerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/CompressedExplorerAdapter.java @@ -53,10 +53,10 @@ import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; -import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.Toast; +import androidx.appcompat.widget.AppCompatImageButton; +import androidx.appcompat.widget.AppCompatImageView; import androidx.recyclerview.widget.RecyclerView; /** Created by Arpit on 25-01-2015 edited by Emmanuel Messulam */ @@ -127,7 +127,7 @@ public ArrayList getCheckedItemPositions() { * @param position the position of the item * @param imageView the circular {@link CircleGradientDrawable} that is to be animated */ - private void toggleChecked(int position, ImageView imageView) { + private void toggleChecked(int position, AppCompatImageView imageView) { compressedExplorerFragment.stopAnim(); stoppedAnimation = true; @@ -204,7 +204,7 @@ public CompressedItemViewHolder onCreateViewHolder(ViewGroup parent, int viewTyp } else if (viewType == TYPE_ITEM) { View v = mInflater.inflate(R.layout.rowlayout, parent, false); CompressedItemViewHolder vh = new CompressedItemViewHolder(v); - ImageButton about = v.findViewById(R.id.properties); + AppCompatImageButton about = v.findViewById(R.id.properties); about.setVisibility(View.INVISIBLE); return vh; } else { 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 7e180188ae..bffe1cdd6c 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -84,15 +84,15 @@ import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; -import android.widget.ImageView; import android.widget.PopupMenu; -import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.view.ActionMode; import androidx.appcompat.view.ContextThemeWrapper; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.AppCompatTextView; import androidx.recyclerview.widget.RecyclerView; /** @@ -200,7 +200,7 @@ public RecyclerAdapter( * @param position the position of the item * @param imageView the check {@link CircleGradientDrawable} that is to be animated */ - public void toggleChecked(int position, ImageView imageView) { + public void toggleChecked(int position, AppCompatImageView imageView) { if (getItemsDigested().size() <= position || position < 0) { AppConfig.toast(context, R.string.operation_not_supported); return; @@ -1192,7 +1192,7 @@ private View getDragShadow(int selectionCount) { .setVisibility(View.VISIBLE); String rememberMovePreference = sharedPrefs.getString(PreferencesConstants.PREFERENCE_DRAG_AND_DROP_REMEMBERED, ""); - ImageView icon = + AppCompatImageView icon = mainFragment .getMainActivity() .getTabFragment() @@ -1204,7 +1204,7 @@ private View getDragShadow(int selectionCount) { .getTabFragment() .getDragPlaceholder() .findViewById(R.id.files_count_parent); - TextView filesCount = + AppCompatTextView filesCount = mainFragment .getMainActivity() .getTabFragment() @@ -1238,7 +1238,7 @@ private int getDragIconReference(String rememberMovePreference) { private void showThumbnailWithBackground( ItemViewHolder viewHolder, IconDataParcelable iconData, - ImageView view, + AppCompatImageView view, OnImageProcessed errorListener) { if (iconData.isImageBroken()) { viewHolder.genericIcon.setVisibility(View.VISIBLE); @@ -1301,7 +1301,7 @@ public boolean onResourceReady( private void showRoundedThumbnail( ItemViewHolder viewHolder, IconDataParcelable iconData, - ImageView view, + AppCompatImageView view, OnImageProcessed errorListener) { if (iconData.isImageBroken()) { View iconBackground = diff --git a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt index c523453385..d9ea4c87ce 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt @@ -24,7 +24,7 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -81,8 +81,8 @@ class SearchRecyclerViewAdapter : inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val fileNameTV: TextView - val filePathTV: TextView + val fileNameTV: AppCompatTextView + val filePathTV: AppCompatTextView val colorView: View init { diff --git a/app/src/main/java/com/amaze/filemanager/adapters/glide/AppsAdapterPreloadModel.java b/app/src/main/java/com/amaze/filemanager/adapters/glide/AppsAdapterPreloadModel.java index 53d98db748..c76d130cfc 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/glide/AppsAdapterPreloadModel.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/glide/AppsAdapterPreloadModel.java @@ -36,10 +36,10 @@ import android.content.Context; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; -import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; @@ -85,7 +85,7 @@ public RequestBuilder getPreloadRequestBuilder(String item) { } } - public void loadApkImage(String item, ImageView v) { + public void loadApkImage(String item, AppCompatImageView v) { if (isBottomSheet) { request.load(getApplicationIconFromPackageName(item)).into(v); } else { diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt index f5ef96a37f..71edf51820 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/AppHolder.kt @@ -22,10 +22,10 @@ package com.amaze.filemanager.adapters.holders import android.view.View import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.ImageView import android.widget.RelativeLayout -import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageButton +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginTop @@ -36,7 +36,7 @@ import com.amaze.filemanager.utils.Utils class AppHolder(view: View) : RecyclerView.ViewHolder(view) { @JvmField - val apkIcon: ImageView = view.findViewById(R.id.apk_icon) + val apkIcon: AppCompatImageView = view.findViewById(R.id.apk_icon) @JvmField val txtTitle: ThemedTextView = view.findViewById(R.id.firstline) @@ -45,16 +45,16 @@ class AppHolder(view: View) : RecyclerView.ViewHolder(view) { val rl: RelativeLayout = view.findViewById(R.id.second) @JvmField - val txtDesc: TextView = view.findViewById(R.id.date) + val txtDesc: AppCompatTextView = view.findViewById(R.id.date) @JvmField - val about: ImageButton = view.findViewById(R.id.properties) + val about: AppCompatImageButton = view.findViewById(R.id.properties) @JvmField val summary: RelativeLayout = view.findViewById(R.id.summary) @JvmField - val packageName: TextView = view.findViewById(R.id.appManagerPackageName) + val packageName: AppCompatTextView = view.findViewById(R.id.appManagerPackageName) init { apkIcon.visibility = View.VISIBLE @@ -69,7 +69,7 @@ class AppHolder(view: View) : RecyclerView.ViewHolder(view) { ) txtDesc.layoutParams = layoutParams - view.findViewById(R.id.picture_icon).visibility = View.GONE - view.findViewById(R.id.generic_icon).visibility = View.GONE + view.findViewById(R.id.picture_icon).visibility = View.GONE + view.findViewById(R.id.generic_icon).visibility = View.GONE } } diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/CompressedItemViewHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/CompressedItemViewHolder.kt index a49cd50a0c..bf1dc4d78d 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/CompressedItemViewHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/CompressedItemViewHolder.kt @@ -21,8 +21,8 @@ package com.amaze.filemanager.adapters.holders import android.view.View -import android.widget.ImageView -import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R import com.amaze.filemanager.ui.views.ThemedTextView @@ -30,28 +30,28 @@ import com.amaze.filemanager.ui.views.ThemedTextView class CompressedItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { // each data item is just a string in this case @JvmField - val pictureIcon: ImageView = view.findViewById(R.id.picture_icon) + val pictureIcon: AppCompatImageView = view.findViewById(R.id.picture_icon) @JvmField - val genericIcon: ImageView = view.findViewById(R.id.generic_icon) + val genericIcon: AppCompatImageView = view.findViewById(R.id.generic_icon) @JvmField - val apkIcon: ImageView = view.findViewById(R.id.apk_icon) + val apkIcon: AppCompatImageView = view.findViewById(R.id.apk_icon) @JvmField val txtTitle: ThemedTextView = view.findViewById(R.id.firstline) @JvmField - val txtDesc: TextView = view.findViewById(R.id.secondLine) + val txtDesc: AppCompatTextView = view.findViewById(R.id.secondLine) @JvmField - val date: TextView = view.findViewById(R.id.date) + val date: AppCompatTextView = view.findViewById(R.id.date) - val perm: TextView = view.findViewById(R.id.permis) + val perm: AppCompatTextView = view.findViewById(R.id.permis) @JvmField val rl: View = view.findViewById(R.id.second) @JvmField - val checkImageView: ImageView = view.findViewById(R.id.check_icon) + val checkImageView: AppCompatImageView = view.findViewById(R.id.check_icon) } diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/DonationViewHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/DonationViewHolder.kt index c2593c297f..21a7a223d6 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/DonationViewHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/DonationViewHolder.kt @@ -22,7 +22,7 @@ package com.amaze.filemanager.adapters.holders import android.view.View import android.widget.LinearLayout -import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R @@ -31,11 +31,11 @@ class DonationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val ROOT_VIEW: LinearLayout = itemView.findViewById(R.id.adapter_donation_root) @JvmField - val TITLE: TextView = itemView.findViewById(R.id.adapter_donation_title) + val TITLE: AppCompatTextView = itemView.findViewById(R.id.adapter_donation_title) @JvmField - val SUMMARY: TextView = itemView.findViewById(R.id.adapter_donation_summary) + val SUMMARY: AppCompatTextView = itemView.findViewById(R.id.adapter_donation_summary) @JvmField - val PRICE: TextView = itemView.findViewById(R.id.adapter_donation_price) + val PRICE: AppCompatTextView = itemView.findViewById(R.id.adapter_donation_price) } diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/HiddenViewHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/HiddenViewHolder.kt index 0b19831a9f..23cdc016d9 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/HiddenViewHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/HiddenViewHolder.kt @@ -21,9 +21,9 @@ package com.amaze.filemanager.adapters.holders import android.view.View -import android.widget.ImageButton import android.widget.LinearLayout -import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageButton +import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R @@ -34,13 +34,13 @@ import com.amaze.filemanager.R */ class HiddenViewHolder(view: View) : RecyclerView.ViewHolder(view) { @JvmField - val deleteButton: ImageButton = view.findViewById(R.id.delete_button) + val deleteButton: AppCompatImageButton = view.findViewById(R.id.delete_button) @JvmField - val textTitle: TextView = view.findViewById(R.id.filename) + val textTitle: AppCompatTextView = view.findViewById(R.id.filename) @JvmField - val textDescription: TextView = view.findViewById(R.id.file_path) + val textDescription: AppCompatTextView = view.findViewById(R.id.file_path) @JvmField val row: LinearLayout = view.findViewById(R.id.bookmarkrow) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/ItemViewHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/ItemViewHolder.kt index 3681602210..a0e1685460 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/ItemViewHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/ItemViewHolder.kt @@ -21,10 +21,10 @@ package com.amaze.filemanager.adapters.holders import android.view.View -import android.widget.ImageButton -import android.widget.ImageView import android.widget.RelativeLayout -import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageButton +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R import com.amaze.filemanager.ui.views.ThemedTextView @@ -36,43 +36,43 @@ import com.amaze.filemanager.ui.views.ThemedTextView class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { // each data item is just a string in this case @JvmField - val pictureIcon: ImageView? = view.findViewById(R.id.picture_icon) + val pictureIcon: AppCompatImageView? = view.findViewById(R.id.picture_icon) @JvmField - val genericIcon: ImageView = view.findViewById(R.id.generic_icon) + val genericIcon: AppCompatImageView = view.findViewById(R.id.generic_icon) @JvmField - val apkIcon: ImageView? = view.findViewById(R.id.apk_icon) + val apkIcon: AppCompatImageView? = view.findViewById(R.id.apk_icon) @JvmField - val imageView1: ImageView? = view.findViewById(R.id.icon_thumb) + val imageView1: AppCompatImageView? = view.findViewById(R.id.icon_thumb) @JvmField val txtTitle: ThemedTextView = view.findViewById(R.id.firstline) @JvmField - val txtDesc: TextView = view.findViewById(R.id.secondLine) + val txtDesc: AppCompatTextView = view.findViewById(R.id.secondLine) @JvmField - val date: TextView = view.findViewById(R.id.date) + val date: AppCompatTextView = view.findViewById(R.id.date) @JvmField - val perm: TextView = view.findViewById(R.id.permis) + val perm: AppCompatTextView = view.findViewById(R.id.permis) @JvmField val baseItemView: View = view.findViewById(R.id.second) @JvmField - val genericText: TextView? = view.findViewById(R.id.generictext) + val genericText: AppCompatTextView? = view.findViewById(R.id.generictext) @JvmField - val about: ImageButton = view.findViewById(R.id.properties) + val about: AppCompatImageButton = view.findViewById(R.id.properties) @JvmField - val checkImageView: ImageView? = view.findViewById(R.id.check_icon) + val checkImageView: AppCompatImageView? = view.findViewById(R.id.check_icon) @JvmField - val checkImageViewGrid: ImageView? = view.findViewById(R.id.check_icon_grid) + val checkImageViewGrid: AppCompatImageView? = view.findViewById(R.id.check_icon_grid) @JvmField val iconLayout: RelativeLayout? = view.findViewById(R.id.icon_frame_grid) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/holders/SpecialViewHolder.kt b/app/src/main/java/com/amaze/filemanager/adapters/holders/SpecialViewHolder.kt index ec4a3e128e..0fc7f10bb0 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/holders/SpecialViewHolder.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/holders/SpecialViewHolder.kt @@ -22,7 +22,7 @@ package com.amaze.filemanager.adapters.holders import android.content.Context import android.view.View -import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R import com.amaze.filemanager.ui.provider.UtilitiesProvider @@ -39,7 +39,7 @@ class SpecialViewHolder( val type: Int ) : RecyclerView.ViewHolder(view) { // each data item is just a string in this case - private val txtTitle: TextView = view.findViewById(R.id.text) + private val txtTitle: AppCompatTextView = view.findViewById(R.id.text) companion object { const val HEADER_FILES = 0 diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CountItemsOrAndSizeTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CountItemsOrAndSizeTask.java index 2319ef3c30..a6e63e1304 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CountItemsOrAndSizeTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CountItemsOrAndSizeTask.java @@ -29,8 +29,8 @@ import android.content.Context; import android.os.AsyncTask; import android.text.format.Formatter; -import android.widget.TextView; +import androidx.appcompat.widget.AppCompatTextView; import androidx.core.util.Pair; /** @@ -39,12 +39,12 @@ public class CountItemsOrAndSizeTask extends AsyncTask, String> { private Context context; - private TextView itemsText; + private AppCompatTextView itemsText; private HybridFileParcelable file; private boolean isStorage; public CountItemsOrAndSizeTask( - Context c, TextView itemsText, HybridFileParcelable f, boolean storage) { + Context c, AppCompatTextView itemsText, HybridFileParcelable f, boolean storage) { this.context = c; this.itemsText = itemsText; file = f; diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashTask.kt index 486c2cfacf..43e0dc27bb 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashTask.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashTask.kt @@ -23,8 +23,8 @@ package com.amaze.filemanager.asynchronous.asynctasks.hashcalculator import android.content.Context import android.view.View import android.widget.LinearLayout -import android.widget.TextView import android.widget.Toast +import androidx.appcompat.widget.AppCompatTextView import com.amaze.filemanager.R import com.amaze.filemanager.asynchronous.asynctasks.Task import com.amaze.filemanager.filesystem.HybridFileParcelable @@ -32,7 +32,7 @@ import com.amaze.filemanager.filesystem.files.FileUtils import org.slf4j.Logger import org.slf4j.LoggerFactory import java.lang.ref.WeakReference -import java.util.* +import java.util.Locale import java.util.concurrent.Callable data class Hash(val md5: String, val sha: String) @@ -78,8 +78,8 @@ class CalculateHashTask( val md5Text = hashes?.md5 ?: context.getString(R.string.unavailable) val shaText = hashes?.sha ?: context.getString(R.string.unavailable) - val md5HashText = view.findViewById(R.id.t9) - val sha256Text = view.findViewById(R.id.t10) + val md5HashText = view.findViewById(R.id.t9) + val sha256Text = view.findViewById(R.id.t10) val mMD5LinearLayout = view.findViewById(R.id.properties_dialog_md5) val mSHA256LinearLayout = view.findViewById(R.id.properties_dialog_sha256) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PrepareCopyTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PrepareCopyTask.java index 4bd15f349a..9d06ddb19d 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PrepareCopyTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PrepareCopyTask.java @@ -52,10 +52,10 @@ import android.os.AsyncTask; import android.view.LayoutInflater; import android.view.View; -import android.widget.CheckBox; import android.widget.Toast; import androidx.annotation.IntDef; +import androidx.appcompat.widget.AppCompatCheckBox; /** * This AsyncTask works by creating a tree where each folder that can be fusioned together with @@ -222,7 +222,7 @@ private void showDialog( copyDialogBinding.fileNameText.setText(conflictingFiles.get(counter).getName(context.get())); // checkBox - final CheckBox checkBox = copyDialogBinding.checkBox; + final AppCompatCheckBox checkBox = copyDialogBinding.checkBox; Utils.setTint(context.get(), checkBox, accentColor); dialogBuilder.theme(mainActivity.get().getAppTheme().getMaterialDialogTheme(context.get())); dialogBuilder.title(context.get().getResources().getString(R.string.paste)); diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java index 74f426a53e..e836e9cb94 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/ExtractService.java @@ -54,12 +54,12 @@ import android.os.AsyncTask; import android.os.IBinder; import android.text.TextUtils; -import android.widget.EditText; import android.widget.RemoteViews; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.appcompat.widget.AppCompatEditText; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.preference.PreferenceManager; @@ -391,7 +391,7 @@ protected void onProgressUpdate(IOException... values) { R.string.archive_password_prompt, R.string.authenticate_password, (dialog, which) -> { - EditText editText = dialog.getView().findViewById(R.id.singleedittext_input); + AppCompatEditText editText = dialog.getView().findViewById(R.id.singleedittext_input); ArchivePasswordCache.getInstance().put(compressedPath, editText.getText().toString()); ExtractService.this.getDataPackages().clear(); this.paused = false; diff --git a/app/src/main/java/com/amaze/filemanager/crashreport/ErrorActivity.java b/app/src/main/java/com/amaze/filemanager/crashreport/ErrorActivity.java index ade83ff7b2..7921a34c40 100644 --- a/app/src/main/java/com/amaze/filemanager/crashreport/ErrorActivity.java +++ b/app/src/main/java/com/amaze/filemanager/crashreport/ErrorActivity.java @@ -58,13 +58,13 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.AppCompatButton; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.appcompat.widget.AppCompatTextView; import androidx.appcompat.widget.Toolbar; import androidx.core.app.NavUtils; @@ -109,7 +109,7 @@ public class ErrorActivity extends ThemedActivity { private ErrorInfo errorInfo; private Class returnActivity; private String currentTimeStamp; - private EditText userCommentBox; + private AppCompatEditText userCommentBox; public static void reportError( final Context context, @@ -197,14 +197,14 @@ public void onCreate(final Bundle savedInstanceState) { actionBar.setDisplayShowTitleEnabled(true); } - final Button reportEmailButton = findViewById(R.id.errorReportEmailButton); - final Button reportTelegramButton = findViewById(R.id.errorReportTelegramButton); - final Button copyButton = findViewById(R.id.errorReportCopyButton); - final Button reportGithubButton = findViewById(R.id.errorReportGitHubButton); + final AppCompatButton reportEmailButton = findViewById(R.id.errorReportEmailButton); + final AppCompatButton reportTelegramButton = findViewById(R.id.errorReportTelegramButton); + final AppCompatButton copyButton = findViewById(R.id.errorReportCopyButton); + final AppCompatButton reportGithubButton = findViewById(R.id.errorReportGitHubButton); userCommentBox = findViewById(R.id.errorCommentBox); - final TextView errorView = findViewById(R.id.errorView); - final TextView errorMessageView = findViewById(R.id.errorMessageView); + final AppCompatTextView errorView = findViewById(R.id.errorView); + final AppCompatTextView errorMessageView = findViewById(R.id.errorMessageView); returnActivity = MainActivity.class; errorInfo = intent.getParcelableExtra(ERROR_INFO); @@ -306,8 +306,8 @@ private void goToReturnActivity() { } private void buildInfo(final ErrorInfo info) { - final TextView infoLabelView = findViewById(R.id.errorInfoLabelsView); - final TextView infoView = findViewById(R.id.errorInfosView); + final AppCompatTextView infoLabelView = findViewById(R.id.errorInfoLabelsView); + final AppCompatTextView infoView = findViewById(R.id.errorInfosView); String text = ""; infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n")); @@ -440,7 +440,7 @@ private String getOsString() { private void addGuruMeditation() { // just an easter egg - final TextView sorryView = findViewById(R.id.errorSorryView); + final AppCompatTextView sorryView = findViewById(R.id.errorSorryView); String text = sorryView.getText().toString(); text += "\n" + getString(R.string.guru_meditation); sorryView.setText(text); diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/EncryptDecryptUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/EncryptDecryptUtils.java index b2418779c6..8744369041 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/EncryptDecryptUtils.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/EncryptDecryptUtils.java @@ -50,10 +50,10 @@ import android.content.SharedPreferences; import android.os.Build; import android.util.Base64; -import android.widget.EditText; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatEditText; import androidx.preference.PreferenceManager; /** @@ -114,7 +114,7 @@ public static void decryptFile( R.string.crypt_decrypt, R.string.authenticate_password, (dialog, which) -> { - EditText editText = dialog.getView().findViewById(R.id.singleedittext_input); + AppCompatEditText editText = dialog.getView().findViewById(R.id.singleedittext_input); decryptIntent.putExtra(EncryptService.TAG_PASSWORD, editText.getText().toString()); ServiceWatcherUtil.runService(main.getContext(), decryptIntent); dialog.dismiss(); diff --git a/app/src/main/java/com/amaze/filemanager/ui/Extensions.kt b/app/src/main/java/com/amaze/filemanager/ui/Extensions.kt index 8b2b0f8013..98d28b1d7f 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/Extensions.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/Extensions.kt @@ -27,8 +27,8 @@ import android.content.pm.PackageManager import android.text.TextUtils import android.view.View import android.view.inputmethod.InputMethodManager -import android.widget.EditText import android.widget.Toast +import androidx.appcompat.widget.AppCompatEditText import com.amaze.filemanager.R import com.amaze.filemanager.application.AppConfig import com.google.android.material.textfield.TextInputLayout @@ -82,7 +82,7 @@ fun Context.updateAUAlias(shouldEnable: Boolean) { /** * Force keyboard pop up on focus */ -fun EditText.openKeyboard(context: Context) { +fun AppCompatEditText.openKeyboard(context: Context) { this.requestFocus() this.postDelayed( diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java index 5bd40d7128..7c35f30838 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java @@ -49,9 +49,9 @@ import android.os.Bundle; import android.view.MenuItem; import android.view.View; -import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.FileProvider; @@ -66,7 +66,7 @@ public class AboutActivity extends ThemedActivity implements View.OnClickListene private AppBarLayout mAppBarLayout; private CollapsingToolbarLayout mCollapsingToolbarLayout; - private TextView mTitleTextView; + private AppCompatTextView mTitleTextView; private View mAuthorsDivider, mDeveloper1Divider; private Billing billing; diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/texteditor/TextEditorActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/texteditor/TextEditorActivity.java index 51f86a430e..f3bebaf489 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/texteditor/TextEditorActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/texteditor/TextEditorActivity.java @@ -70,20 +70,20 @@ import android.view.animation.AccelerateDecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.ImageButton; import android.widget.RelativeLayout; import android.widget.ScrollView; import android.widget.Toast; import androidx.annotation.ColorInt; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.appcompat.widget.AppCompatImageButton; import androidx.lifecycle.ViewModelProvider; public class TextEditorActivity extends ThemedActivity implements TextWatcher, View.OnClickListener { - public EditText mainTextView; - public EditText searchEditText; + public AppCompatEditText mainTextView; + public AppCompatEditText searchEditText; private Typeface inputTypefaceDefault; private Typeface inputTypefaceMono; private androidx.appcompat.widget.Toolbar toolbar; @@ -96,9 +96,9 @@ public class TextEditorActivity extends ThemedActivity private static final String KEY_MONOFONT = "monofont"; private RelativeLayout searchViewLayout; - public ImageButton upButton; - public ImageButton downButton; - public ImageButton closeButton; + public AppCompatImageButton upButton; + public AppCompatImageButton downButton; + public AppCompatImageButton closeButton; private Snackbar loadingSnackbar; diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/ColorPickerDialog.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/ColorPickerDialog.java index 18aa5742b3..977ba264e4 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/ColorPickerDialog.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/ColorPickerDialog.java @@ -43,10 +43,10 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.RadioButton; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.StringRes; +import androidx.appcompat.widget.AppCompatTextView; import androidx.core.util.Pair; import androidx.preference.Preference.BaseSavedState; import androidx.preference.PreferenceDialogFragmentCompat; @@ -167,7 +167,7 @@ public void onBindDialogView(View view) { select(selectedItem, true); } - ((TextView) child.findViewById(R.id.text)).setText(COLORS[i].first); + ((AppCompatTextView) child.findViewById(R.id.text)).setText(COLORS[i].first); CircularColorsView colorsView = child.findViewById(R.id.circularColorsView); colorsView.setColors(getColor(i, 0), getColor(i, 1), getColor(i, 2), getColor(i, 3)); AppTheme appTheme = @@ -185,7 +185,7 @@ public void onBindDialogView(View view) { select(selectedItem, true); } - ((TextView) child.findViewById(R.id.text)).setText(R.string.custom); + ((AppCompatTextView) child.findViewById(R.id.text)).setText(R.string.custom); child.findViewById(R.id.circularColorsView).setVisibility(View.INVISIBLE); container.addView(child); } @@ -197,7 +197,7 @@ public void onBindDialogView(View view) { select(selectedItem, true); } - ((TextView) child.findViewById(R.id.text)).setText(R.string.random); + ((AppCompatTextView) child.findViewById(R.id.text)).setText(R.string.random); child.findViewById(R.id.circularColorsView).setVisibility(View.INVISIBLE); container.addView(child); } @@ -249,9 +249,9 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { ((UserColorPreferences) requireArguments().getParcelable(ARG_COLOR_PREF)).getAccent(); // Button views - ((TextView) dialog.findViewById(res.getIdentifier("button1", "id", "android"))) + ((AppCompatTextView) dialog.findViewById(res.getIdentifier("button1", "id", "android"))) .setTextColor(accentColor); - ((TextView) dialog.findViewById(res.getIdentifier("button2", "id", "android"))) + ((AppCompatTextView) dialog.findViewById(res.getIdentifier("button2", "id", "android"))) .setTextColor(accentColor); return dialog; diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/DecryptFingerprintDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/DecryptFingerprintDialog.kt index 149a24a319..5647c930de 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/DecryptFingerprintDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/DecryptFingerprintDialog.kt @@ -25,8 +25,8 @@ import android.content.Intent import android.hardware.fingerprint.FingerprintManager import android.os.Build import android.view.View -import android.widget.Button import androidx.annotation.RequiresApi +import androidx.appcompat.widget.AppCompatButton import com.afollestad.materialdialogs.MaterialDialog import com.amaze.filemanager.R import com.amaze.filemanager.filesystem.files.CryptUtil @@ -62,7 +62,9 @@ object DecryptFingerprintDialog { val builder = MaterialDialog.Builder(c) builder.title(c.getString(R.string.crypt_decrypt)) val rootView = View.inflate(c, R.layout.dialog_decrypt_fingerprint_authentication, null) - val cancelButton = rootView.findViewById

Options
MethodOption TypeDescription
BZIP2NumberBlock Size - an number between 1 and 9
DEFLATENumberCompression Level - an number between 1 and 9