diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..c73ba27988 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Supported Versions + +`v3.8.5` supports Android 4.0 and above. +`v4.x.x` would only support Android 4.4 and above. + +Android devices that runs Android versions less that Android 4.4 +(Android KitKat) would not receive any more updates (the latest +supported version would be `v3.8.5`). + +## Reporting a Vulnerability + +Feel free to contact us via `support@teamamaze.xyz`. + +Please CC the maintainers too: +- `vishalmeham2@gmail.com` +- `airwave209gt@gmail.com` +- `emmanuelbendavid@gmail.com` +- `t.v.s10123@gmail.com` \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index ee99efc317..f1e2672e8b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,7 +143,7 @@ dependencies { testImplementation "androidx.test:rules:$androidXTestVersion" testImplementation "androidx.test.ext:junit:$androidXTestExtVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion" - testImplementation "org.mockito:mockito-inline:$mockitoVersion" + testImplementation "org.mockito:mockito-inline:$mockitoInlineVersion" testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" testImplementation "org.apache.sshd:sshd-core:1.7.0" testImplementation "org.awaitility:awaitility:$awaitilityVersion" 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 60460b7c2b..66e7445d45 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt @@ -253,6 +253,7 @@ class AppsRecyclerAdapter( ) { uri, mimeType, useNewStack -> val intent = buildIntent( + fragment.requireContext(), uri, mimeType, useNewStack, 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..1d2445bdfb 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -21,6 +21,7 @@ package com.amaze.filemanager.adapters; import static com.amaze.filemanager.filesystem.compressed.CompressedHelper.*; +import static com.amaze.filemanager.filesystem.files.FileListSorter.SORT_NONE_ON_TOP; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_COLORIZE_ICONS; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_FILE_SIZE; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_GOBACK_BUTTON; @@ -31,7 +32,9 @@ import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_USE_CIRCULAR_IMAGES; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,8 +50,10 @@ import com.amaze.filemanager.adapters.holders.SpecialViewHolder; import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; +import com.amaze.filemanager.filesystem.PasteHelper; import com.amaze.filemanager.filesystem.files.CryptUtil; import com.amaze.filemanager.ui.ItemPopupMenu; +import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.activities.superclasses.PreferenceActivity; import com.amaze.filemanager.ui.colors.ColorUtils; import com.amaze.filemanager.ui.drag.RecyclerAdapterDragListener; @@ -62,6 +67,7 @@ import com.amaze.filemanager.ui.views.CircleGradientDrawable; import com.amaze.filemanager.utils.AnimUtils; import com.amaze.filemanager.utils.GlideConstants; +import com.amaze.filemanager.utils.MainActivityActionMode; import com.amaze.filemanager.utils.Utils; import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader; import com.bumptech.glide.load.DataSource; @@ -84,15 +90,16 @@ 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 android.widget.Toast; 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 +207,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; @@ -374,6 +381,19 @@ public void toggleSameDates() { } } + public void toggleFill() { + ArrayList checkedItemsIndexes = getCheckedItemsIndex(); + Collections.sort(checkedItemsIndexes); + if (checkedItemsIndexes.size() >= 2) { + for (int i = checkedItemsIndexes.get(0); + i < checkedItemsIndexes.get(checkedItemsIndexes.size() - 1); + i++) { + Objects.requireNonNull(getItemsDigested()).get(i).setChecked(true); + notifyItemChanged(i); + } + } + } + public void toggleSimilarNames() { ArrayList checkedItemsIndexes = getCheckedItemsIndex(); for (int i = 0; i < checkedItemsIndexes.size(); i++) { @@ -605,34 +625,43 @@ private void setItems( } public void createHeaders(boolean invalidate, List uris) { - boolean[] headers = new boolean[] {false, false}; + if ((mainFragment.getMainFragmentViewModel() != null + && mainFragment.getMainFragmentViewModel().getDsort() == SORT_NONE_ON_TOP) + || getItemsDigested() == null + || getItemsDigested().isEmpty()) { + return; + } else { + boolean[] headers = new boolean[] {false, false}; - for (int i = 0; i < getItemsDigested().size(); i++) { + for (int i = 0; i < getItemsDigested().size(); i++) { - if (getItemsDigested().get(i).layoutElementParcelable != null) { - LayoutElementParcelable nextItem = getItemsDigested().get(i).layoutElementParcelable; + if (getItemsDigested().get(i).layoutElementParcelable != null) { + LayoutElementParcelable nextItem = getItemsDigested().get(i).layoutElementParcelable; - if (!headers[0] && nextItem.isDirectory) { - headers[0] = true; - getItemsDigested().add(i, new ListItem(TYPE_HEADER_FOLDERS)); - uris.add(i, null); - continue; - } + if (nextItem != null) { + if (!headers[0] && nextItem.isDirectory) { + headers[0] = true; + getItemsDigested().add(i, new ListItem(TYPE_HEADER_FOLDERS)); + uris.add(i, null); + continue; + } - if (!headers[1] - && !nextItem.isDirectory - && !nextItem.title.equals(".") - && !nextItem.title.equals("..")) { - headers[1] = true; - getItemsDigested().add(i, new ListItem(TYPE_HEADER_FILES)); - uris.add(i, null); - continue; // leave this continue for symmetry + if (!headers[1] + && !nextItem.isDirectory + && !nextItem.title.equals(".") + && !nextItem.title.equals("..")) { + headers[1] = true; + getItemsDigested().add(i, new ListItem(TYPE_HEADER_FILES)); + uris.add(i, null); + continue; // leave this continue for symmetry + } + } } } - } - if (invalidate) { - notifyDataSetChanged(); + if (invalidate) { + notifyDataSetChanged(); + } } } @@ -763,6 +792,7 @@ private void bindViewHolderList(@NonNull final ItemViewHolder holder, int positi holder.baseItemView.setOnLongClickListener( p1 -> { + if (hasPendingPasteOperation()) return false; if (!isBackButton) { if (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_DEFAULT || (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_TO_MOVE_COPY @@ -976,6 +1006,7 @@ private void bindViewHolderGrid(@NonNull final ItemViewHolder holder, int positi holder.baseItemView.setOnLongClickListener( p1 -> { + if (hasPendingPasteOperation()) return false; if (!isBackButton) { if (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_DEFAULT || (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_TO_MOVE_COPY @@ -1192,7 +1223,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 +1235,7 @@ private View getDragShadow(int selectionCount) { .getTabFragment() .getDragPlaceholder() .findViewById(R.id.files_count_parent); - TextView filesCount = + AppCompatTextView filesCount = mainFragment .getMainActivity() .getTabFragment() @@ -1238,7 +1269,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 +1332,7 @@ public boolean onResourceReady( private void showRoundedThumbnail( ItemViewHolder viewHolder, IconDataParcelable iconData, - ImageView view, + AppCompatImageView view, OnImageProcessed errorListener) { if (iconData.isImageBroken()) { View iconBackground = @@ -1366,6 +1397,7 @@ public boolean onResourceReady( } private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelable rowItem) { + if (hasPendingPasteOperation()) return; Context currentContext = this.context; if (mainFragment.getMainActivity().getAppTheme().getSimpleTheme(mainFragment.requireContext()) == AppTheme.BLACK) { @@ -1428,6 +1460,31 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl popupMenu.show(); } + /** + * Helps in deciding whether to allow file modification or not, depending on the state of the + * copy/paste operation. + * + * @return true if there is an unfinished copy/paste operation, false otherwise. + */ + private boolean hasPendingPasteOperation() { + MainActivity mainActivity = mainFragment.getMainActivity(); + if (mainActivity == null) return false; + MainActivityActionMode mainActivityActionMode = mainActivity.mainActivityActionMode; + PasteHelper pasteHelper = mainActivityActionMode.getPasteHelper(); + + if (pasteHelper != null + && pasteHelper.getSnackbar() != null + && pasteHelper.getSnackbar().isShown()) { + Toast.makeText( + mainFragment.requireContext(), + mainFragment.getString(R.string.complete_paste_warning), + Toast.LENGTH_LONG) + .show(); + return true; + } + return false; + } + private boolean getBoolean(String key) { return preferenceActivity.getBoolean(key); } 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/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/LoadFilesListTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java index 4ae437aaa6..fdf51c6b98 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 @@ -22,6 +22,8 @@ import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.Q; +import static com.amaze.filemanager.filesystem.files.FileListSorter.SORT_ASC; +import static com.amaze.filemanager.filesystem.files.FileListSorter.SORT_DSC; import java.io.File; import java.lang.ref.WeakReference; @@ -258,9 +260,9 @@ private void postListCustomPathProcess( if (sortType <= 3) { sortBy = sortType; - isAscending = 1; + isAscending = SORT_ASC; } else { - isAscending = -1; + isAscending = SORT_DSC; sortBy = sortType - 4; } 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..6067e9a7c5 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) @@ -45,9 +45,9 @@ class CalculateHashTask( private val log: Logger = LoggerFactory.getLogger(CalculateHashTask::class.java) - private val task: Callable = if (file.isSftp) { + private val task: Callable = if (file.isSftp && !file.isDirectory(context)) { CalculateHashSftpCallback(file) - } else if (file.isFtp) { + } else if (file.isFtp || file.isDirectory(context)) { // Don't do this. Especially when FTPClient requires thread safety. DoNothingCalculateHashCallback() } else { @@ -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/MoveFiles.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java index a76c2b3a28..cfcabd66b1 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java @@ -47,7 +47,7 @@ /** * AsyncTask that moves files from source to destination by trying to rename files first, if they're * in the same filesystem, else starting the copy service. Be advised - do not start this AsyncTask - * directly but use {@link PrepareCopyTask} instead + * directly but use {@link PreparePasteTask} instead */ public class MoveFiles implements Callable { 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/asynctasks/movecopy/PrepareCopyTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PrepareCopyTask.java deleted file mode 100644 index 4bd15f349a..0000000000 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PrepareCopyTask.java +++ /dev/null @@ -1,446 +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.movecopy; - -import static com.amaze.filemanager.fileoperations.filesystem.FolderStateKt.CAN_CREATE_FILES; -import static com.amaze.filemanager.fileoperations.filesystem.OperationTypeKt.COPY; -import static com.amaze.filemanager.fileoperations.filesystem.OperationTypeKt.MOVE; - -import java.io.File; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Set; - -import com.afollestad.materialdialogs.DialogAction; -import com.afollestad.materialdialogs.MaterialDialog; -import com.amaze.filemanager.R; -import com.amaze.filemanager.asynchronous.asynctasks.TaskKt; -import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil; -import com.amaze.filemanager.asynchronous.services.CopyService; -import com.amaze.filemanager.databinding.CopyDialogBinding; -import com.amaze.filemanager.fileoperations.filesystem.FolderState; -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.FileUtils; -import com.amaze.filemanager.ui.activities.MainActivity; -import com.amaze.filemanager.utils.Utils; - -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.CheckBox; -import android.widget.Toast; - -import androidx.annotation.IntDef; - -/** - * This AsyncTask works by creating a tree where each folder that can be fusioned together with - * another in the destination is a node (CopyNode). While the tree is being created an indeterminate - * ProgressDialog is shown. Each node is copied when the conflicts are dealt with (the dialog is - * shown, and the tree is walked via a BFS). If the process is cancelled (via the button in the - * dialog) the dialog closes without any more code to be executed, finishCopying() is never executed - * so no changes are made. - */ -public class PrepareCopyTask extends AsyncTask { - - private final String path; - private final Boolean move; - private final WeakReference mainActivity; - private final WeakReference context; - private int counter = 0; - private ProgressDialog dialog; - private boolean rootMode = false; - private OpenMode openMode = OpenMode.FILE; - private @DialogState int dialogState = UNKNOWN; - private boolean isRenameMoveSupport = false; - - // causes folder containing filesToCopy to be deleted - private ArrayList deleteCopiedFolder = null; - private CopyNode copyFolder; - private final ArrayList paths = new ArrayList<>(); - private final ArrayList> filesToCopyPerFolder = new ArrayList<>(); - private final ArrayList filesToCopy; // a copy of params sent to this - - private static final int UNKNOWN = -1; - private static final int DO_NOT_REPLACE = 0; - private static final int REPLACE = 1; - - @IntDef({UNKNOWN, DO_NOT_REPLACE, REPLACE}) - @interface DialogState {} - - public PrepareCopyTask( - String path, - Boolean move, - MainActivity con, - boolean rootMode, - OpenMode openMode, - ArrayList filesToCopy) { - this.move = move; - mainActivity = new WeakReference<>(con); - context = new WeakReference<>(con); - this.openMode = openMode; - this.rootMode = rootMode; - this.path = path; - this.filesToCopy = filesToCopy; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - dialog = - ProgressDialog.show(context.get(), "", context.get().getString(R.string.processing), true); - } - - @Override - public void onProgressUpdate(String... message) { - Toast.makeText(context.get(), message[0], Toast.LENGTH_LONG).show(); - } - - @Override - protected CopyNode doInBackground(Void... params) { - long totalBytes = 0; - - if (openMode == OpenMode.OTG - || openMode == OpenMode.DROPBOX - || openMode == OpenMode.BOX - || openMode == OpenMode.GDRIVE - || openMode == OpenMode.ONEDRIVE - || openMode == OpenMode.ROOT) { - // no helper method for OTG to determine storage space - return null; - } - - HybridFile destination = new HybridFile(openMode, path); - destination.generateMode(context.get()); - - if (move - && destination.getMode() == openMode - && MoveFiles.getOperationSupportedFileSystem().contains(openMode)) { - // move/rename supported filesystems, skip checking for space - isRenameMoveSupport = true; - } - - totalBytes = FileUtils.getTotalBytes(filesToCopy, context.get()); - - if (destination.getUsableSpace() < totalBytes && !isRenameMoveSupport) { - publishProgress(context.get().getResources().getString(R.string.in_safe)); - return null; - } - - copyFolder = new CopyNode(path, filesToCopy); - - return copyFolder; - } - - private ArrayList checkConflicts( - final ArrayList filesToCopy, HybridFile destination) { - final ArrayList conflictingFiles = new ArrayList<>(); - destination.forEachChildrenFile( - context.get(), - rootMode, - file -> { - for (HybridFileParcelable j : filesToCopy) { - if (file.getName(context.get()).equals((j).getName(context.get()))) { - conflictingFiles.add(j); - } - } - }); - return conflictingFiles; - } - - @Override - protected void onPostExecute(CopyNode copyFolder) { - super.onPostExecute(copyFolder); - if (openMode == OpenMode.OTG - || openMode == OpenMode.GDRIVE - || openMode == OpenMode.DROPBOX - || openMode == OpenMode.BOX - || openMode == OpenMode.ONEDRIVE - || openMode == OpenMode.ROOT) { - - startService(filesToCopy, path, openMode); - } else { - - if (copyFolder == null) { - // not starting service as there's no sufficient space - dialog.dismiss(); - return; - } - - onEndDialog(null, null, null); - } - - dialog.dismiss(); - } - - private void startService( - ArrayList sourceFiles, String target, OpenMode openmode) { - Intent intent = new Intent(context.get(), CopyService.class); - intent.putParcelableArrayListExtra(CopyService.TAG_COPY_SOURCES, sourceFiles); - intent.putExtra(CopyService.TAG_COPY_TARGET, target); - intent.putExtra(CopyService.TAG_COPY_OPEN_MODE, openmode.ordinal()); - intent.putExtra(CopyService.TAG_COPY_MOVE, move); - intent.putExtra(CopyService.TAG_IS_ROOT_EXPLORER, rootMode); - ServiceWatcherUtil.runService(context.get(), intent); - } - - private void showDialog( - final String path, - final ArrayList filesToCopy, - final ArrayList conflictingFiles) { - int accentColor = mainActivity.get().getAccent(); - final MaterialDialog.Builder dialogBuilder = new MaterialDialog.Builder(context.get()); - CopyDialogBinding copyDialogBinding = - CopyDialogBinding.inflate(LayoutInflater.from(mainActivity.get())); - dialogBuilder.customView(copyDialogBinding.getRoot(), true); - - // textView - copyDialogBinding.fileNameText.setText(conflictingFiles.get(counter).getName(context.get())); - - // checkBox - final CheckBox 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)); - dialogBuilder.positiveText(R.string.skip); - dialogBuilder.negativeText(R.string.overwrite); - dialogBuilder.neutralText(R.string.cancel); - dialogBuilder.positiveColor(accentColor); - dialogBuilder.negativeColor(accentColor); - dialogBuilder.neutralColor(accentColor); - dialogBuilder.onPositive( - (dialog, which) -> { - if (checkBox.isChecked()) dialogState = DO_NOT_REPLACE; - doNotReplaceFiles(path, filesToCopy, conflictingFiles); - }); - dialogBuilder.onNegative( - (dialog, which) -> { - if (checkBox.isChecked()) dialogState = REPLACE; - replaceFiles(path, filesToCopy, conflictingFiles); - }); - - final MaterialDialog dialog = dialogBuilder.build(); - dialog.show(); - if (filesToCopy.get(0).getParent(context.get()).equals(path)) { - View negative = dialog.getActionButton(DialogAction.NEGATIVE); - negative.setEnabled(false); - } - } - - private void onEndDialog( - String path, - ArrayList filesToCopy, - ArrayList conflictingFiles) { - if (conflictingFiles != null - && counter != conflictingFiles.size() - && conflictingFiles.size() > 0) { - if (dialogState == UNKNOWN) { - showDialog(path, filesToCopy, conflictingFiles); - } else if (dialogState == DO_NOT_REPLACE) { - doNotReplaceFiles(path, filesToCopy, conflictingFiles); - } else if (dialogState == REPLACE) { - replaceFiles(path, filesToCopy, conflictingFiles); - } - } else { - CopyNode c = !copyFolder.hasStarted() ? copyFolder.startCopy() : copyFolder.goToNextNode(); - - if (c != null) { - counter = 0; - - paths.add(c.getPath()); - filesToCopyPerFolder.add(c.filesToCopy); - - if (dialogState == UNKNOWN) { - onEndDialog(c.path, c.filesToCopy, c.conflictingFiles); - } else if (dialogState == DO_NOT_REPLACE) { - doNotReplaceFiles(c.path, c.filesToCopy, c.conflictingFiles); - } else if (dialogState == REPLACE) { - replaceFiles(c.path, c.filesToCopy, c.conflictingFiles); - } - } else { - finishCopying(paths, filesToCopyPerFolder); - } - } - } - - private void doNotReplaceFiles( - String path, - ArrayList filesToCopy, - ArrayList conflictingFiles) { - if (counter < conflictingFiles.size()) { - if (dialogState != UNKNOWN) { - filesToCopy.remove(conflictingFiles.get(counter)); - counter++; - } else { - for (int j = counter; j < conflictingFiles.size(); j++) { - filesToCopy.remove(conflictingFiles.get(j)); - } - counter = conflictingFiles.size(); - } - } - - onEndDialog(path, filesToCopy, conflictingFiles); - } - - private void replaceFiles( - String path, - ArrayList filesToCopy, - ArrayList conflictingFiles) { - if (counter < conflictingFiles.size()) { - if (dialogState != UNKNOWN) { - counter++; - } else { - counter = conflictingFiles.size(); - } - } - - onEndDialog(path, filesToCopy, conflictingFiles); - } - - private void finishCopying( - ArrayList paths, ArrayList> filesToCopyPerFolder) { - for (int i = 0; i < filesToCopyPerFolder.size(); i++) { - if (filesToCopyPerFolder.get(i) == null || filesToCopyPerFolder.get(i).size() == 0) { - filesToCopyPerFolder.remove(i); - paths.remove(i); - i--; - } - } - - if (filesToCopyPerFolder.size() != 0) { - @FolderState - int mode = mainActivity.get().mainActivityHelper.checkFolder(path, openMode, context.get()); - if (mode == CAN_CREATE_FILES && !path.contains("otg:/")) { - // This is used because in newer devices the user has to accept a permission, - // see MainActivity.onActivityResult() - mainActivity.get().oparrayListList = filesToCopyPerFolder; - mainActivity.get().oparrayList = null; - mainActivity.get().operation = move ? MOVE : COPY; - mainActivity.get().oppatheList = paths; - } else { - if (!move) { - for (int i = 0; i < filesToCopyPerFolder.size(); i++) { - startService(filesToCopyPerFolder.get(i), paths.get(i), openMode); - } - } else { - TaskKt.fromTask( - new MoveFilesTask( - filesToCopyPerFolder, rootMode, path, context.get(), openMode, paths)); - } - } - } else { - Toast.makeText( - context.get(), - context.get().getResources().getString(R.string.no_file_overwrite), - Toast.LENGTH_SHORT) - .show(); - } - } - - class CopyNode { - private final String path; - private final ArrayList filesToCopy; - private final ArrayList conflictingFiles; - private final ArrayList nextNodes = new ArrayList<>(); - - CopyNode(String p, ArrayList filesToCopy) { - path = p; - this.filesToCopy = filesToCopy; - - HybridFile destination = new HybridFile(openMode, path); - conflictingFiles = checkConflicts(filesToCopy, destination); - - for (int i = 0; i < conflictingFiles.size(); i++) { - if (conflictingFiles.get(i).isDirectory()) { - if (deleteCopiedFolder == null) deleteCopiedFolder = new ArrayList<>(); - - deleteCopiedFolder.add(new File(conflictingFiles.get(i).getPath())); - - nextNodes.add( - new CopyNode( - path + "/" + conflictingFiles.get(i).getName(context.get()), - conflictingFiles.get(i).listFiles(context.get(), rootMode))); - - filesToCopy.remove(filesToCopy.indexOf(conflictingFiles.get(i))); - conflictingFiles.remove(i); - i--; - } - } - } - - /** The next 2 methods are a BFS that runs through one node at a time. */ - private LinkedList queue = null; - - private Set visited = null; - - CopyNode startCopy() { - queue = new LinkedList<>(); - visited = new HashSet<>(); - - queue.add(this); - visited.add(this); - return this; - } - - /** - * @return true if there are no more nodes - */ - CopyNode goToNextNode() { - if (queue.isEmpty()) return null; - else { - CopyNode node = queue.element(); - CopyNode child; - if ((child = getUnvisitedChildNode(visited, node)) != null) { - visited.add(child); - queue.add(child); - return child; - } else { - queue.remove(); - return goToNextNode(); - } - } - } - - boolean hasStarted() { - return queue != null; - } - - String getPath() { - return path; - } - - private CopyNode getUnvisitedChildNode(Set visited, CopyNode node) { - for (CopyNode n : node.nextNodes) { - if (!visited.contains(n)) { - return n; - } - } - - return null; - } - } -} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt new file mode 100644 index 0000000000..0b0fe96f87 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt @@ -0,0 +1,484 @@ +/* + * 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.movecopy + +import android.app.ProgressDialog +import android.content.Intent +import android.view.LayoutInflater +import android.widget.Toast +import androidx.appcompat.widget.AppCompatCheckBox +import com.afollestad.materialdialogs.DialogAction +import com.afollestad.materialdialogs.MaterialDialog +import com.amaze.filemanager.R +import com.amaze.filemanager.asynchronous.asynctasks.fromTask +import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil +import com.amaze.filemanager.asynchronous.services.CopyService +import com.amaze.filemanager.databinding.CopyDialogBinding +import com.amaze.filemanager.fileoperations.filesystem.CAN_CREATE_FILES +import com.amaze.filemanager.fileoperations.filesystem.COPY +import com.amaze.filemanager.fileoperations.filesystem.FolderState +import com.amaze.filemanager.fileoperations.filesystem.MOVE +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.FilenameHelper +import com.amaze.filemanager.filesystem.HybridFile +import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.filesystem.MakeDirectoryOperation +import com.amaze.filemanager.filesystem.files.FileUtils +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.utils.OnFileFound +import com.amaze.filemanager.utils.Utils +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.lang.ref.WeakReference +import java.util.LinkedList + +/** + * This helper class works by checking the conflicts during paste operation. After checking + * conflicts [MaterialDialog] is shown to user for each conflicting file. If the conflicting file + * is a directory, the conflicts are resolved by inserting a node in [CopyNode] tree and then doing + * BFS on this tree. + */ +class PreparePasteTask(strongRefMain: MainActivity) { + + private lateinit var targetPath: String + private var isMove = false + private var isRootMode = false + private lateinit var openMode: OpenMode + private lateinit var filesToCopy: MutableList + + private val pathsList = ArrayList() + private val filesToCopyPerFolder = ArrayList>() + + private val context = WeakReference(strongRefMain) + + @Suppress("DEPRECATION") + private var progressDialog: ProgressDialog? = null + private val coroutineScope = CoroutineScope(Job() + Dispatchers.Default) + + private lateinit var destination: HybridFile + private val conflictingFiles: MutableList = mutableListOf() + private val conflictingDirActionMap = HashMap() + + private var skipAll = false + private var renameAll = false + private var overwriteAll = false + + private fun startService( + sourceFiles: ArrayList, + target: String, + openMode: OpenMode, + isMove: Boolean, + isRootMode: Boolean + ) { + val intent = Intent(context.get(), CopyService::class.java) + intent.putParcelableArrayListExtra(CopyService.TAG_COPY_SOURCES, sourceFiles) + intent.putExtra(CopyService.TAG_COPY_TARGET, target) + intent.putExtra(CopyService.TAG_COPY_OPEN_MODE, openMode.ordinal) + intent.putExtra(CopyService.TAG_COPY_MOVE, isMove) + intent.putExtra(CopyService.TAG_IS_ROOT_EXPLORER, isRootMode) + ServiceWatcherUtil.runService(context.get(), intent) + } + + /** + * Starts execution of [PreparePasteTask] class. + */ + fun execute( + targetPath: String, + isMove: Boolean, + isRootMode: Boolean, + openMode: OpenMode, + filesToCopy: ArrayList + ) { + this.targetPath = targetPath + this.isMove = isMove + this.isRootMode = isRootMode + this.openMode = openMode + this.filesToCopy = filesToCopy + + val isCloudOrRootMode = openMode == OpenMode.OTG || + openMode == OpenMode.GDRIVE || + openMode == OpenMode.DROPBOX || + openMode == OpenMode.BOX || + openMode == OpenMode.ONEDRIVE || + openMode == OpenMode.ROOT + + if (isCloudOrRootMode) { + startService(filesToCopy, targetPath, openMode, isMove, isRootMode) + return + } + + val totalBytes = FileUtils.getTotalBytes(filesToCopy, context.get()) + destination = HybridFile(openMode, targetPath) + destination.generateMode(context.get()) + + if (filesToCopy.isNotEmpty() && + isMove && + filesToCopy[0].getParent(context.get()) == targetPath + ) { + Toast.makeText(context.get(), R.string.same_dir_move_error, Toast.LENGTH_SHORT).show() + return + } + + val isMoveSupported = isMove && + destination.mode == openMode && + MoveFiles.getOperationSupportedFileSystem().contains(openMode) + + if (destination.usableSpace < totalBytes && + !isMoveSupported + ) { + Toast.makeText(context.get(), R.string.in_safe, Toast.LENGTH_SHORT).show() + return + } + @Suppress("DEPRECATION") + progressDialog = ProgressDialog.show( + context.get(), + "", + context.get()?.getString(R.string.checking_conflicts) + ) + checkConflicts( + isRootMode, + filesToCopy, + destination, + conflictingFiles, + conflictingDirActionMap + ) + } + + private fun checkConflicts( + isRootMode: Boolean, + filesToCopy: ArrayList, + destination: HybridFile, + conflictingFiles: MutableList, + conflictingDirActionMap: HashMap + ) { + coroutineScope.launch { + destination.forEachChildrenFile( + context.get(), + isRootMode, + object : OnFileFound { + override fun onFileFound(file: HybridFileParcelable) { + for (fileToCopy in filesToCopy) { + if (file.getName(context.get()) == fileToCopy.getName(context.get())) { + conflictingFiles.add(fileToCopy) + } + } + } + } + ) + withContext(Dispatchers.Main) { + prepareDialog(conflictingFiles, conflictingDirActionMap) + @Suppress("DEPRECATION") + progressDialog?.setMessage(context.get()?.getString(R.string.copying)) + } + resolveConflict(conflictingFiles, conflictingDirActionMap, filesToCopy) + } + } + + private suspend fun prepareDialog( + conflictingFiles: MutableList, + conflictingDirActionMap: HashMap + ) { + if (conflictingFiles.isEmpty()) return + + val contextRef = context.get() ?: return + val accentColor = contextRef.accent + val dialogBuilder = MaterialDialog.Builder(contextRef) + val copyDialogBinding: CopyDialogBinding = + CopyDialogBinding.inflate(LayoutInflater.from(contextRef)) + dialogBuilder.customView(copyDialogBinding.root, true) + val checkBox: AppCompatCheckBox = copyDialogBinding.checkBox + + Utils.setTint(contextRef, checkBox, accentColor) + dialogBuilder.theme(contextRef.appTheme.getMaterialDialogTheme(contextRef)) + dialogBuilder.title(contextRef.resources.getString(R.string.paste)) + dialogBuilder.positiveText(R.string.rename) + dialogBuilder.neutralText(R.string.skip) + dialogBuilder.positiveColor(accentColor) + dialogBuilder.negativeColor(accentColor) + dialogBuilder.neutralColor(accentColor) + dialogBuilder.negativeText(R.string.overwrite) + dialogBuilder.cancelable(false) + showDialog( + conflictingFiles, + conflictingDirActionMap, + copyDialogBinding, + dialogBuilder, + checkBox + ) + } + + private suspend fun showDialog( + conflictingFiles: MutableList, + conflictingDirActionMap: HashMap, + copyDialogBinding: CopyDialogBinding, + dialogBuilder: MaterialDialog.Builder, + checkBox: AppCompatCheckBox + ) { + val iterator = conflictingFiles.iterator() + while (iterator.hasNext()) { + val hybridFileParcelable = iterator.next() + copyDialogBinding.fileNameText.text = hybridFileParcelable.name + val dialog = dialogBuilder.build() + if (hybridFileParcelable.getParent(context.get()) == targetPath) { + dialog.getActionButton(DialogAction.NEGATIVE) + .isEnabled = false + } + val resultDeferred = CompletableDeferred() + dialogBuilder.onPositive { _, _ -> + resultDeferred.complete(DialogAction.POSITIVE) + } + dialogBuilder.onNegative { _, _ -> + resultDeferred.complete(DialogAction.NEGATIVE) + } + dialogBuilder.onNeutral { _, _ -> + resultDeferred.complete(DialogAction.NEUTRAL) + } + dialog.show() + when (resultDeferred.await()) { + DialogAction.POSITIVE -> { + if (checkBox.isChecked) { + renameAll = true + return + } + conflictingDirActionMap[hybridFileParcelable] = Action.RENAME + } + DialogAction.NEGATIVE -> { + if (checkBox.isChecked) { + overwriteAll = true + return + } + conflictingDirActionMap[hybridFileParcelable] = Action.OVERWRITE + } + DialogAction.NEUTRAL -> { + if (checkBox.isChecked) { + skipAll = true + return + } + conflictingDirActionMap[hybridFileParcelable] = Action.SKIP + } + } + iterator.remove() + } + } + + private fun resolveConflict( + conflictingFiles: MutableList, + conflictingDirActionMap: HashMap, + filesToCopy: ArrayList + ) = coroutineScope.launch { + var index = conflictingFiles.size - 1 + if (renameAll) { + while (conflictingFiles.isNotEmpty()) { + conflictingDirActionMap[conflictingFiles[index]] = Action.RENAME + conflictingFiles.removeAt(index) + index-- + } + } else if (overwriteAll) { + while (conflictingFiles.isNotEmpty()) { + conflictingDirActionMap[conflictingFiles[index]] = Action.OVERWRITE + conflictingFiles.removeAt(index) + index-- + } + } else if (skipAll) { + while (conflictingFiles.isNotEmpty()) { + filesToCopy.remove(conflictingFiles.removeAt(index)) + index-- + } + } + + val rootNode = CopyNode(targetPath, ArrayList(filesToCopy)) + var currentNode: CopyNode? = rootNode.startCopy() + + while (currentNode != null) { + pathsList.add(currentNode.path) + filesToCopyPerFolder.add(currentNode.filesToCopy) + currentNode = rootNode.goToNextNode() + } + finishCopying() + } + + private suspend fun finishCopying() { + var index = 0 + while (index < filesToCopyPerFolder.size) { + if (filesToCopyPerFolder[index].size == 0) { + filesToCopyPerFolder.removeAt(index) + pathsList.removeAt(index) + index-- + } + index++ + } + if (filesToCopyPerFolder.isNotEmpty()) { + @FolderState + val mode: Int = context.get()?.mainActivityHelper!! + .checkFolder(targetPath, openMode, context.get()) + if (mode == CAN_CREATE_FILES && !targetPath.contains("otg:/")) { + // This is used because in newer devices the user has to accept a permission, + // see MainActivity.onActivityResult() + context.get()?.oparrayListList = filesToCopyPerFolder + context.get()?.oparrayList = null + context.get()?.operation = if (isMove) MOVE else COPY + context.get()?.oppatheList = pathsList + } else { + if (!isMove) { + for (foldersIndex in filesToCopyPerFolder.indices) + startService( + filesToCopyPerFolder[foldersIndex], + pathsList[foldersIndex], + openMode, + isMove, + isRootMode + ) + } else { + fromTask( + MoveFilesTask( + filesToCopyPerFolder, + isRootMode, + targetPath, + context.get()!!, + openMode, + pathsList + ) + ) + } + } + } else { + withContext(Dispatchers.Main) { + Toast.makeText( + context.get(), + context.get()!!.resources.getString(R.string.no_file_overwrite), + Toast.LENGTH_SHORT + ).show() + } + } + withContext(Dispatchers.Main) { + progressDialog?.dismiss() + } + coroutineScope.cancel() + } + + private inner class CopyNode( + val path: String, + val filesToCopy: ArrayList + ) { + private val nextNodes: MutableList = mutableListOf() + private var queue: LinkedList? = null + private var visited: HashSet? = null + + init { + val iterator = filesToCopy.iterator() + while (iterator.hasNext()) { + val hybridFileParcelable = iterator.next() + if (conflictingDirActionMap.contains(hybridFileParcelable)) { + val fileAtTarget = HybridFile( + hybridFileParcelable.mode, + path, + hybridFileParcelable.name, + hybridFileParcelable.isDirectory + ) + when (conflictingDirActionMap[hybridFileParcelable]) { + Action.RENAME -> { + if (hybridFileParcelable.isDirectory) { + val newName = + FilenameHelper.increment(fileAtTarget).getName(context.get()) + val newPath = "$path/$newName" + val newDir = HybridFile(hybridFileParcelable.mode, newPath) + MakeDirectoryOperation.mkdirs(context.get()!!, newDir) + @Suppress("DEPRECATION") + nextNodes.add( + CopyNode( + newPath, + hybridFileParcelable.listFiles(context.get(), isRootMode) + ) + ) + iterator.remove() + } else { + filesToCopy[filesToCopy.indexOf(hybridFileParcelable)].name = + FilenameHelper.increment( + fileAtTarget + ).getName(context.get()) + } + } + + Action.SKIP -> iterator.remove() + } + } + } + } + + /** + * Starts BFS traversal of tree. + * + * @return Root node + */ + fun startCopy(): CopyNode { + queue = LinkedList() + visited = HashSet() + queue!!.add(this) + visited!!.add(this) + return this + } + + /** + * Moves to the next unvisited node in tree. + * + * @return The next unvisited node if available, otherwise returns null. + */ + fun goToNextNode(): CopyNode? = + if (queue.isNullOrEmpty()) null + else { + val node = queue!!.element() + val child = getUnvisitedChildNode(visited!!, node) + if (child != null) { + visited!!.add(child) + queue!!.add(child) + child + } else { + queue!!.remove() + goToNextNode() + } + } + + private fun getUnvisitedChildNode( + visited: Set, + node: CopyNode + ): CopyNode? { + for (currentNode in node.nextNodes) { + if (!visited.contains(currentNode)) { + return currentNode + } + } + return null + } + } + + private class Action { + companion object { + const val SKIP = "skip" + const val RENAME = "rename" + const val OVERWRITE = "overwrite" + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt index 8ba17a597f..513bc0e5d6 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt @@ -93,9 +93,11 @@ class FileHandler( !mainFragmentViewModel.isList ) } else { - val itemList = main.elementsList ?: listOf() - // we already have some elements in list view, invalidate the adapter - (listView.adapter as RecyclerAdapter).setItems(listView, itemList) + listView.adapter?.let { + val itemList = main.elementsList ?: listOf() + // we already have some elements in list view, invalidate the adapter + (listView.adapter as RecyclerAdapter).setItems(listView, itemList) + } } } else { // there was no list view, means the directory was empty 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 fbb0ae012a..2ddfca97e3 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; @@ -461,7 +462,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/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/FileUtil.java b/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java index 3d129a9fdc..8e7012aad4 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java @@ -105,7 +105,7 @@ public static OutputStream getOutputStream(final File target, Context context) /** Writes uri stream from external application to the specified path */ public static final void writeUriToStorage( @NonNull final MainActivity mainActivity, - @NonNull final ArrayList uris, + @NonNull final List uris, @NonNull final ContentResolver contentResolver, @NonNull final String currentPath) { diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/FilenameHelper.kt b/app/src/main/java/com/amaze/filemanager/filesystem/FilenameHelper.kt new file mode 100644 index 0000000000..d1e97ddc4d --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/FilenameHelper.kt @@ -0,0 +1,239 @@ +/* + * 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.filesystem + +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.SLASH +import kotlin.math.absoluteValue + +/** + * Convenient extension to return path element of a path string = the part before the last slash. + */ +fun String.pathDirname(): String = if (contains(SLASH)) { + substringBeforeLast(SLASH) +} else { + "" +} + +/** + * Convenient extension to return the name element of a path = the part after the last slash. + */ +fun String.pathBasename(): String = if (contains(SLASH)) { + substringAfterLast(SLASH) +} else { + this +} + +/** + * Convenient extension to return the basename element of a filename = the part after the last + * slash and before the extension (.). + */ +fun String.pathFileBasename(): String = if (contains('.')) { + pathBasename().substringBeforeLast('.') +} else { + pathBasename() +} + +/** + * Convenient extension to return the extension element of a filename = the part after the last + * slash and after the extension (.). Returns empty string if no extension dot exist. + */ +fun String.pathFileExtension(): String = if (contains('.')) { + pathBasename().substringAfterLast('.') +} else { + "" +} + +enum class FilenameFormatFlag { + DARWIN, DEFAULT, WINDOWS, LINUX +} + +object FilenameHelper { + + /* Don't split complex regexs into multiple lines. */ + + /* ktlint-disable max-line-length */ + private const val REGEX_RAW_NUMBERS = "| [0-9]+" + private const val REGEX_SOURCE = " \\((?:(another|[0-9]+(th|st|nd|rd)) )?copy\\)|copy( [0-9]+)?|\\.\\(incomplete\\)| \\([0-9]+\\)|[- ]+" + /* ktlint-enable max-line-length */ + + private val ordinals = arrayOf("th", "st", "nd", "rd") + + /** + * Strip the file path to one without increments or numbers. + * + * Default will not strip the raw numbers; specify removeRawNumbers = true to do so. + */ + @JvmStatic + fun strip(input: String, removeRawNumbers: Boolean = false): String { + val filepath = stripIncrementInternal(input, removeRawNumbers) + val extension = filepath.pathFileExtension() + val dirname = stripIncrementInternal(filepath.pathDirname(), removeRawNumbers) + val stem = stem(filepath, removeRawNumbers) + return StringBuilder().run { + if (dirname.isNotBlank()) { + append(dirname).append(SLASH) + } + append(stem) + if (extension.isNotBlank()) { + append('.').append(extension) + } + toString() + } + } + + /** + * Returns the ordinals of the given number. So that + * + * - toOrdinal(1) returns "1st" + * - toOrdinal(2) returns "2nd" + * - toOrdinal(10) returns "10th" + * - toOrdinal(11) returns "11th" + * - toOrdinal(12) returns "12th" + * - toOrdinal(21) returns "21st" + * - toOrdinal(22) returns "22nd" + * - toOrdinal(23) returns "23rd" + * + * etc. + */ + @JvmStatic + fun toOrdinal(n: Int): String = "$n${ordinal(n.absoluteValue)}" + + /** + * Increment the filename of a given [HybridFile]. + * + * Uses [HybridFile.exists] to check file existence and if it exists, returns a HybridFile + * with new filename which does not exist. + */ + @JvmStatic + fun increment( + file: HybridFile, + platform: FilenameFormatFlag = FilenameFormatFlag.DEFAULT, + strip: Boolean = true, + removeRawNumbers: Boolean = false, + startArg: Int = 1 + ): HybridFile { + var filename = file.getName(AppConfig.getInstance()) + var dirname = file.path.pathDirname() + var basename = filename.pathFileBasename() + val extension = filename.pathFileExtension() + + var start: Int = startArg + + if (strip) { + filename = stripIncrementInternal(filename, removeRawNumbers) + dirname = stripIncrementInternal(dirname, removeRawNumbers) + basename = strip(basename, removeRawNumbers) + } + + var retval = HybridFile( + file.mode, + dirname, + filename, + file.isDirectory(AppConfig.getInstance()) + ) + + while (retval.exists(AppConfig.getInstance())) { + filename = if (extension.isNotBlank()) { + format(platform, basename, start++) + ".$extension" + } else { + format(platform, basename, start++) + } + retval = HybridFile( + file.mode, + dirname, + filename, + file.isDirectory(AppConfig.getInstance()) + ) + } + + return retval + } + + private fun stripIncrementInternal(input: String, removeRawNumbers: Boolean = false): String { + val source = StringBuilder().run { + append(REGEX_SOURCE) + if (removeRawNumbers) { + append(REGEX_RAW_NUMBERS) + } + toString() + } + return Regex("($source)+$", RegexOption.IGNORE_CASE).replace(input, "") + } + + private fun stem(filepath: String, removeRawNumbers: Boolean = false): String { + val extension = filepath.pathFileExtension() + return stripIncrementInternal( + filepath.pathBasename().substringBefore(".$extension"), + removeRawNumbers + ) + } + + private fun ordinal(n: Int): String { + var retval = ordinals.getOrNull(((n % 100) - 20) % 10) + if (retval == null) { + retval = ordinals.getOrNull(n % 100) + } + if (retval == null) { + retval = ordinals[0] + } + return retval + } + + // TODO: i18n + private fun format(flag: FilenameFormatFlag, stem: String, n: Int): String { + return when (flag) { + FilenameFormatFlag.DARWIN -> { + if (n == 1) { + "$stem copy" + } else if (n > 1) { + "$stem copy $n" + } else { + stem + } + } + FilenameFormatFlag.LINUX -> { + when (n) { + 0 -> { + stem + } + 1 -> { + "$stem (copy)" + } + 2 -> { + "$stem (another copy)" + } + else -> { + "$stem (${toOrdinal(n)} copy)" + } + } + } + // Windows and default formatting are the same. + else -> { + if (n >= 1) { + "$stem ($n)" + } else { + stem + } + } + } + } +} 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 2f91894d0d..d67d7873fc 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -26,6 +26,8 @@ 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 static com.amaze.filemanager.filesystem.ssh.SFTPClientExtKt.READ_AHEAD_MAX_UNCONFIRMED_READS; +import static com.amaze.filemanager.filesystem.ssh.SshClientUtils.sftpGetSize; import java.io.File; import java.io.FileInputStream; @@ -39,10 +41,7 @@ import java.net.URLDecoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Calendar; import java.util.EnumSet; import java.util.List; import java.util.Locale; @@ -74,6 +73,7 @@ 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.SFTPClientExtKt; import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate; import com.amaze.filemanager.filesystem.ssh.SshClientSessionTemplate; import com.amaze.filemanager.filesystem.ssh.SshClientUtils; @@ -82,7 +82,6 @@ import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.utils.DataUtils; -import com.amaze.filemanager.utils.Function; import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.OnFileFound; import com.amaze.filemanager.utils.Utils; @@ -94,15 +93,16 @@ import android.content.Context; import android.net.Uri; import android.os.Build; +import android.text.TextUtils; import android.text.format.Formatter; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; 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; @@ -119,6 +119,7 @@ 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; @@ -302,11 +303,11 @@ public long lastModified() { switch (mode) { case SFTP: final Long returnValue = - NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { + SshClientUtils.execute( + new SFtpClientTemplate(path, true) { @Override public Long execute(@NonNull SFTPClient client) throws IOException { - return client.mtime(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path)); + return client.mtime(NetCopyClientUtils.extractRemotePathFrom(path)); } }); @@ -350,13 +351,7 @@ public long length(Context context) { if (this instanceof HybridFileParcelable) { return ((HybridFileParcelable) this).getSize(); } else { - return NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { - @Override - public Long execute(@NonNull SFTPClient client) throws IOException { - return client.size(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path)); - } - }); + return sftpGetSize.invoke(getPath()); } case SMB: s = @@ -416,13 +411,17 @@ 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"); + return URLDecoder.decode(path, "UTF-8"); } catch (UnsupportedEncodingException | IllegalArgumentException e) { LOG.warn("failed to decode path {}", path, e); return path; @@ -513,8 +512,7 @@ public FTPFile getFtpFile() { new FtpClientTemplate(path, false) { public FTPFile executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOException { String path = - NetCopyClientUtils.INSTANCE.extractRemotePathFrom( - getParent(AppConfig.getInstance())); + NetCopyClientUtils.extractRemotePathFrom(getParent(AppConfig.getInstance())); ftpClient.changeWorkingDirectory(path); for (FTPFile ftpFile : ftpClient.listFiles()) { if (ftpFile.getName().equals(getName(AppConfig.getInstance()))) return ftpFile; @@ -561,14 +559,18 @@ public String getParent(Context context) { if (thisPath.isEmpty() || pathSegments.isEmpty()) return null; String currentName = pathSegments.get(pathSegments.size() - 1); - String parent = thisPath.substring(0, thisPath.lastIndexOf(currentName)); + int currentNameStartIndex = thisPath.lastIndexOf(currentName); + if (currentNameStartIndex < 0) { + return null; + } + String parent = thisPath.substring(0, currentNameStartIndex); if (ArraysKt.any(ANDROID_DATA_DIRS, dir -> parent.endsWith(dir + "/"))) { return FileProperties.unmapPathForApi30OrAbove(parent); } else { return parent; } default: - if (getPath().length() == getName(context).length()) { + if (getPath().length() <= getName(context).length()) { return null; } @@ -618,17 +620,16 @@ public boolean isDirectory() { } public boolean isDirectory(Context context) { - boolean isDirectory; switch (mode) { case SFTP: final Boolean returnValue = - NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { + SshClientUtils.execute( + new SFtpClientTemplate(path, true) { @Override public Boolean execute(@NonNull SFTPClient client) { try { return client - .stat(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path)) + .stat(NetCopyClientUtils.extractRemotePathFrom(path)) .getType() .equals(FileMode.Type.DIRECTORY); } catch (IOException notFound) { @@ -640,56 +641,46 @@ public Boolean execute(@NonNull SFTPClient client) { if (returnValue == null) { LOG.error("Error obtaining if path is directory over SFTP"); + return false; } - //noinspection SimplifiableConditionalExpression - return returnValue == null ? false : returnValue; + return returnValue; case SMB: try { - isDirectory = - Single.fromCallable(() -> getSmbFile().isDirectory()) - .subscribeOn(Schedulers.io()) - .blockingGet(); + return Single.fromCallable(() -> getSmbFile().isDirectory()) + .subscribeOn(Schedulers.io()) + .blockingGet(); } catch (Exception e) { - isDirectory = false; LOG.warn("failed to get isDirectory with context for smb file", e); + return false; } - break; case FTP: FTPFile ftpFile = getFtpFile(); - isDirectory = ftpFile != null && ftpFile.isDirectory(); - break; - case FILE: - isDirectory = getFile().isDirectory(); - break; + return ftpFile != null && ftpFile.isDirectory(); case ROOT: - isDirectory = NativeOperations.isDirectory(path); - break; + return NativeOperations.isDirectory(path); case DOCUMENT_FILE: - isDirectory = getDocumentFile(false).isDirectory(); - break; + DocumentFile documentFile = getDocumentFile(false); + return documentFile != null && documentFile.isDirectory(); case OTG: - isDirectory = OTGUtil.getDocumentFile(path, context, false).isDirectory(); - break; + DocumentFile otgFile = OTGUtil.getDocumentFile(path, context, false); + return otgFile != null && otgFile.isDirectory(); case DROPBOX: case BOX: case GDRIVE: case ONEDRIVE: - isDirectory = - Single.fromCallable( - () -> - dataUtils - .getAccount(mode) - .getMetadata(CloudUtil.stripPath(mode, path)) - .getFolder()) - .subscribeOn(Schedulers.io()) - .blockingGet(); - break; - default: - isDirectory = getFile().isDirectory(); - break; + return Single.fromCallable( + () -> + dataUtils + .getAccount(mode) + .getMetadata(CloudUtil.stripPath(mode, path)) + .getFolder()) + .subscribeOn(Schedulers.io()) + .blockingGet(); + default: // also handles the case `FILE` + File file = getFile(); + return file != null && file.isDirectory(); } - return isDirectory; } /** @@ -726,20 +717,24 @@ public long folderSize(Context context) { switch (mode) { case SFTP: - final Long returnValue = - NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { - @Override - public Long execute(@NonNull SFTPClient client) throws IOException { - return client.size(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path)); - } - }); - - if (returnValue == null) { - LOG.error("Error obtaining size of folder over SFTP"); + Long retval = -1L; + String result = SshClientUtils.execute(getRemoteShellCommandLineResult("du -bs \"%s\"")); + if (!TextUtils.isEmpty(result) && result.indexOf('\t') > 0) { + try { + retval = Long.valueOf(result.substring(0, result.lastIndexOf('\t'))); + } catch (NumberFormatException ifParseFailed) { + LOG.warn("Unable to parse result (Seen {\"\"}), resort to old method", result); + retval = -1L; + } } - - return returnValue == null ? 0L : returnValue; + if (retval == -1L) { + Long returnValue = sftpGetSize.invoke(getPath()); + if (returnValue == null) { + LOG.error("Error obtaining size of folder over SFTP"); + } + return returnValue == null ? 0L : returnValue; + } + return retval; case SMB: SmbFile smbFile = getSmbFile(); size = (smbFile != null) ? FileUtils.folderSize(smbFile) : 0L; @@ -805,8 +800,8 @@ public long getUsableSpace() { break; case SFTP: final Long returnValue = - NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { + SshClientUtils.execute( + new SFtpClientTemplate(path, true) { @Override public Long execute(@NonNull SFTPClient client) throws IOException { try { @@ -817,8 +812,7 @@ public Long execute(@NonNull SFTPClient client) throws IOException { .getSFTPEngine() .request( Statvfs.request( - client, - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path))) + client, NetCopyClientUtils.extractRemotePathFrom(path))) .retrieve()); return response.diskFreeSpace(); } catch (SFTPException e) { @@ -891,8 +885,8 @@ public long getTotal(Context context) { break; case SFTP: final Long returnValue = - NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { + SshClientUtils.execute( + new SFtpClientTemplate(path, true) { @Override public Long execute(@NonNull SFTPClient client) throws IOException { try { @@ -903,8 +897,7 @@ public Long execute(@NonNull SFTPClient client) throws IOException { .getSFTPEngine() .request( Statvfs.request( - client, - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path))) + client, NetCopyClientUtils.extractRemotePathFrom(path))) .retrieve()); return response.diskSize(); } catch (SFTPException e) { @@ -941,40 +934,30 @@ public Long execute(@NonNull SFTPClient client) throws IOException { public void forEachChildrenFile(Context context, boolean isRoot, OnFileFound onFileFound) { switch (mode) { case SFTP: - NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(getPath(), false) { + SshClientUtils.execute( + new SFtpClientTemplate(getPath(), true) { @Override public Boolean execute(@NonNull SFTPClient client) { try { - 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(); + for (RemoteResourceInfo info : + client.ls(NetCopyClientUtils.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); + } } catch (IOException e) { LOG.warn("IOException", e); AppConfig.toast( context, context.getString( R.string.cannot_read_directory, - parseAndFormatUriForDisplay(path), + parseAndFormatUriForDisplay(getPath()), e.getMessage())); } return true; @@ -1002,7 +985,7 @@ public Boolean execute(@NonNull SFTPClient client) { } break; case FTP: - String thisPath = NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath()); + String thisPath = NetCopyClientUtils.extractRemotePathFrom(getPath()); FTPFile[] ftpFiles = NetCopyClientUtils.INSTANCE.execute( new FtpClientTemplate(getPath(), false) { @@ -1097,13 +1080,18 @@ public InputStream getInputStream(Context context) { @Override public InputStream execute(@NonNull final SFTPClient client) throws IOException { final RemoteFile rf = - client.open(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath())); - return rf.new RemoteFileInputStream() { + SFTPClientExtKt.openWithReadAheadSupport( + client, NetCopyClientUtils.extractRemotePathFrom(getPath())); + return rf.new ReadAheadRemoteFileInputStream(READ_AHEAD_MAX_UNCONFIRMED_READS) { @Override public void close() throws IOException { try { + LOG.debug("Closing input stream for {}", getPath()); super.close(); + } catch (Throwable e) { + e.printStackTrace(); } finally { + LOG.debug("Closing client for {}", getPath()); rf.close(); client.close(); } @@ -1138,7 +1126,7 @@ public InputStream executeWithFtpClient(@NonNull FTPClient ftpClient) File tmpFile = File.createTempFile("ftp-transfer_", ".tmp"); tmpFile.deleteOnExit(); ftpClient.changeWorkingDirectory( - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(parent)); + NetCopyClientUtils.extractRemotePathFrom(parent)); ftpClient.setFileType(FTP.BINARY_FILE_TYPE); InputStream fin = ftpClient.retrieveFileStream(getName(AppConfig.getInstance())); @@ -1196,14 +1184,14 @@ public OutputStream getOutputStream(Context context) { OutputStream outputStream; switch (mode) { case SFTP: - return NetCopyClientUtils.INSTANCE.execute( + return SshClientUtils.execute( new SFtpClientTemplate(getPath(), false) { @Nullable @Override public OutputStream execute(@NonNull SFTPClient client) throws IOException { final RemoteFile rf = client.open( - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath()), + NetCopyClientUtils.extractRemotePathFrom(getPath()), EnumSet.of( net.schmizz.sshj.sftp.OpenMode.WRITE, net.schmizz.sshj.sftp.OpenMode.CREAT)); @@ -1231,7 +1219,7 @@ public void close() throws IOException { public OutputStream executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOException { ftpClient.setFileType(FTP.BINARY_FILE_TYPE); - String remotePath = NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path); + String remotePath = NetCopyClientUtils.extractRemotePathFrom(path); OutputStream outputStream = ftpClient.storeFileStream(remotePath); if (outputStream != null) { return FTPClientImpl.wrap(outputStream, ftpClient); @@ -1289,8 +1277,7 @@ public boolean exists() { @Override public Boolean execute(SFTPClient client) throws IOException { try { - return client.stat(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path)) - != null; + return client.stat(NetCopyClientUtils.extractRemotePathFrom(path)) != null; } catch (SFTPException notFound) { return false; } @@ -1393,13 +1380,28 @@ public boolean setLastModified(final long date) { new FtpClientTemplate(path, false) { public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOException { - Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(date); - DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US); - df.setCalendar(calendar); return ftpClient.setModificationTime( - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path), - df.format(calendar.getTime())); + NetCopyClientUtils.extractRemotePathFrom(path), + NetCopyClientUtils.getTimestampForTouch(date)); + } + })); + } else if (isSftp()) { + return Boolean.TRUE.equals( + SshClientUtils.execute( + new SshClientSessionTemplate(getPath()) { + @Override + public Boolean execute(@NonNull Session session) throws IOException { + Session.Command cmd = + session.exec( + String.format( + Locale.US, + "touch -m -t %s \"%s\"", + NetCopyClientUtils.getTimestampForTouch(date), + getPath())); + // Quirk: need to wait the command to finish + IOUtils.readFully(cmd.getInputStream()); + cmd.close(); + return 0 == cmd.getExitStatus(); } })); } else { @@ -1410,12 +1412,12 @@ public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) public void mkdir(Context context) { if (isSftp()) { - NetCopyClientUtils.INSTANCE.execute( + SshClientUtils.execute( new SFtpClientTemplate(path, true) { @Override public Boolean execute(@NonNull SFTPClient client) { try { - client.mkdir(NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path)); + client.mkdir(NetCopyClientUtils.extractRemotePathFrom(path)); } catch (IOException e) { LOG.error("Error making directory over SFTP", e); } @@ -1427,7 +1429,7 @@ public Boolean execute(@NonNull SFTPClient client) { new FtpClientTemplate(getPath(), false) { public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOException { ExtensionsKt.makeDirectoryTree( - ftpClient, NetCopyClientUtils.INSTANCE.extractRemotePathFrom(getPath())); + ftpClient, NetCopyClientUtils.extractRemotePathFrom(getPath())); return true; } }); @@ -1471,11 +1473,11 @@ public boolean delete(Context context, boolean rootmode) throws ShellNotRunningException, SmbException { if (isSftp()) { Boolean retval = - NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(path, false) { + SshClientUtils.execute( + new SFtpClientTemplate(path, true) { @Override public Boolean execute(@NonNull SFTPClient client) throws IOException { - String _path = NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path); + String _path = NetCopyClientUtils.extractRemotePathFrom(path); if (isDirectory(AppConfig.getInstance())) client.rmdir(_path); else client.rm(_path); return client.statExistence(_path) == null; @@ -1489,8 +1491,7 @@ public Boolean execute(@NonNull SFTPClient client) throws IOException { @Override public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOException { - return ftpClient.deleteFile( - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path)); + return ftpClient.deleteFile(NetCopyClientUtils.extractRemotePathFrom(path)); } }); return retval != null && retval; @@ -1643,7 +1644,7 @@ public void getMd5Checksum(Context context, Function callback) { switch (mode) { case SFTP: String md5Command = "md5sum -b \"%s\" | cut -c -32"; - return SshClientUtils.execute(getSftpHash(md5Command)); + return SshClientUtils.execute(getRemoteShellCommandLineResult(md5Command)); default: byte[] b = createChecksum(context); String result = ""; @@ -1685,7 +1686,7 @@ public void getSha256Checksum(Context context, Function callback) switch (mode) { case SFTP: String shaCommand = "sha256sum -b \"%s\" | cut -c -64"; - return SshClientUtils.execute(getSftpHash(shaCommand)); + return SshClientUtils.execute(getRemoteShellCommandLineResult(shaCommand)); default: MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); byte[] input = new byte[GenericCopyUtil.DEFAULT_BUFFER_SIZE]; @@ -1733,11 +1734,11 @@ public void onError(Throwable e) { }); } - private SshClientSessionTemplate getSftpHash(String command) { + private SshClientSessionTemplate getRemoteShellCommandLineResult(String command) { return new SshClientSessionTemplate(path) { @Override public String execute(Session session) throws IOException { - String extractedPath = NetCopyClientUtils.INSTANCE.extractRemotePathFrom(path); + String extractedPath = NetCopyClientUtils.extractRemotePathFrom(getPath()); String fullCommand = String.format(command, extractedPath); Session.Command cmd = session.exec(fullCommand); String result = new String(IOUtils.readFully(cmd.getInputStream()).toByteArray()); diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java index 50928e1e6b..20edfc696e 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java @@ -99,6 +99,11 @@ public HybridFileParcelable(String path, boolean isDirectory, RemoteResourceInfo Integer.toString(FilePermission.toMask(sshFile.getAttributes().getPermissions()), 8)); } + @Override + public long lastModified() { + return date; + } + public String getName() { if (!Utils.isNullOrEmpty(name)) return name; else return super.getSimpleName(); diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt b/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt index 5d44a4a697..1765130374 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt @@ -76,6 +76,12 @@ object MakeDirectoryOperation { } else false } + /** + * Creates the directories on given [file] path, including nonexistent parent directories. + * So use proper [HybridFile] constructor as per your need. + * + * @return true if successfully created directory, otherwise returns false. + */ @JvmStatic fun mkdirs(context: Context, file: HybridFile): Boolean { var isSuccessful = true 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..d247ee43e2 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java @@ -41,12 +41,14 @@ 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; import com.amaze.filemanager.filesystem.root.MakeFileCommand; import com.amaze.filemanager.filesystem.root.RenameFileCommand; import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate; +import com.amaze.filemanager.filesystem.ssh.SshClientUtils; import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; import com.cloudrail.si.interfaces.CloudStorage; @@ -67,7 +69,7 @@ public class Operations { - private static Executor executor = AsyncTask.THREAD_POOL_EXECUTOR; + private static final Executor executor = AsyncTask.THREAD_POOL_EXECUTOR; private static final Logger LOG = LoggerFactory.getLogger(Operations.class); @@ -579,14 +581,14 @@ protected Void doInBackground(Void... params) { } return null; } else if (oldFile.isSftp()) { - NetCopyClientUtils.INSTANCE.execute( - new SFtpClientTemplate(oldFile.getPath(), false) { + SshClientUtils.execute( + new SFtpClientTemplate(oldFile.getPath(), true) { @Override public Void execute(@NonNull SFTPClient client) { try { client.rename( - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(oldFile.getPath()), - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(newFile.getPath())); + NetCopyClientUtils.extractRemotePathFrom(oldFile.getPath()), + NetCopyClientUtils.extractRemotePathFrom(newFile.getPath())); errorCallBack.done(newFile, true); } catch (IOException e) { String errmsg = @@ -620,8 +622,8 @@ public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOException { boolean result = ftpClient.rename( - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(oldFile.getPath()), - NetCopyClientUtils.INSTANCE.extractRemotePathFrom(newFile.getPath())); + NetCopyClientUtils.extractRemotePathFrom(oldFile.getPath()), + NetCopyClientUtils.extractRemotePathFrom(newFile.getPath())); errorCallBack.done(newFile, result); return result; } @@ -715,7 +717,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/PasteHelper.java b/app/src/main/java/com/amaze/filemanager/filesystem/PasteHelper.java index ac83ec6726..2113c1c220 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/PasteHelper.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/PasteHelper.java @@ -29,14 +29,13 @@ import org.slf4j.LoggerFactory; import com.amaze.filemanager.R; -import com.amaze.filemanager.asynchronous.asynctasks.movecopy.PrepareCopyTask; +import com.amaze.filemanager.asynchronous.asynctasks.movecopy.PreparePasteTask; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.fragments.MainFragment; import com.amaze.filemanager.utils.Utils; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; -import android.os.AsyncTask; import android.os.Parcel; import android.os.Parcelable; import android.text.Spanned; @@ -168,14 +167,13 @@ public void onSuccess(Spanned spanned) { ArrayList arrayList = new ArrayList<>(Arrays.asList(paths)); boolean move = operation == PasteHelper.OPERATION_CUT; - new PrepareCopyTask( + new PreparePasteTask(mainActivity) + .execute( path, move, - mainActivity, mainActivity.isRootExplorer(), mainFragment.getMainFragmentViewModel().getOpenMode(), - arrayList) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + arrayList); dismissSnackbar(true); }, () -> dismissSnackbar(true)); 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/filesystem/files/FileListSorter.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.java deleted file mode 100644 index e97e83b59a..0000000000 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.java +++ /dev/null @@ -1,126 +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.files; - -import java.util.Comparator; - -import com.amaze.filemanager.adapters.data.LayoutElementParcelable; - -public class FileListSorter implements Comparator { - - private int dirsOnTop = 0; - private int asc = 1; - private int sort = 0; - - public FileListSorter(int dir, int sort, int asc) { - this.dirsOnTop = dir; - this.asc = asc; - this.sort = sort; - } - - private boolean isDirectory(LayoutElementParcelable path) { - return path.isDirectory; - } - - /** - * Compares two elements and return negative, zero and positive integer if first argument is less - * than, equal to or greater than second - */ - @Override - public int compare(LayoutElementParcelable file1, LayoutElementParcelable file2) { - - /*File f1; - - if(!file1.hasSymlink()) { - - f1=new File(file1.getDesc()); - } else { - f1=new File(file1.getSymlink()); - } - - File f2; - - if(!file2.hasSymlink()) { - - f2=new File(file2.getDesc()); - } else { - f2=new File(file1.getSymlink()); - }*/ - - if (dirsOnTop == 0) { - if (isDirectory(file1) && !isDirectory(file2)) { - return -1; - - } else if (isDirectory(file2) && !isDirectory(file1)) { - return 1; - } - } else if (dirsOnTop == 1) { - if (isDirectory(file1) && !isDirectory(file2)) { - - return 1; - } else if (isDirectory(file2) && !isDirectory(file1)) { - return -1; - } - } - - if (sort == 0) { - - // sort by name - return asc * file1.title.compareToIgnoreCase(file2.title); - } else if (sort == 1) { - - // sort by last modified - return asc * Long.valueOf(file1.date).compareTo(file2.date); - } else if (sort == 2) { - - // sort by size - if (!file1.isDirectory && !file2.isDirectory) { - - return asc * Long.valueOf(file1.longSize).compareTo(file2.longSize); - } else { - - return file1.title.compareToIgnoreCase(file2.title); - } - - } else if (sort == 3) { - - // sort by type - if (!file1.isDirectory && !file2.isDirectory) { - - final String ext_a = getExtension(file1.title); - final String ext_b = getExtension(file2.title); - - final int res = asc * ext_a.compareTo(ext_b); - if (res == 0) { - return asc * file1.title.compareToIgnoreCase(file2.title); - } - return res; - } else { - return file1.title.compareToIgnoreCase(file2.title); - } - } - return 0; - } - - private static String getExtension(String a) { - return a.substring(a.lastIndexOf(".") + 1).toLowerCase(); - } -} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt new file mode 100644 index 0000000000..aac05a8f85 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt @@ -0,0 +1,151 @@ +/* + * 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.files + +import androidx.annotation.IntDef +import com.amaze.filemanager.adapters.data.LayoutElementParcelable +import java.util.Locale + +/** + * [Comparator] implementation to sort [LayoutElementParcelable]s. + */ +class FileListSorter( + @DirSortMode dirArg: Int, + @SortBy sortArg: Int, + @SortOrder ascArg: Int +) : Comparator { + private var dirsOnTop = dirArg + private val asc = ascArg + private val sort = sortArg + + private fun isDirectory(path: LayoutElementParcelable): Boolean { + return path.isDirectory + } + + /** + * Compares two elements and return negative, zero and positive integer if first argument is less + * than, equal to or greater than second + */ + override fun compare(file1: LayoutElementParcelable, file2: LayoutElementParcelable): Int { + /*File f1; + + if(!file1.hasSymlink()) { + + f1=new File(file1.getDesc()); + } else { + f1=new File(file1.getSymlink()); + } + + File f2; + + if(!file2.hasSymlink()) { + + f2=new File(file2.getDesc()); + } else { + f2=new File(file1.getSymlink()); + }*/ + if (dirsOnTop == SORT_DIR_ON_TOP) { + if (isDirectory(file1) && !isDirectory(file2)) { + return -1 + } else if (isDirectory(file2) && !isDirectory(file1)) { + return 1 + } + } else if (dirsOnTop == SORT_FILE_ON_TOP) { + if (isDirectory(file1) && !isDirectory(file2)) { + return 1 + } else if (isDirectory(file2) && !isDirectory(file1)) { + return -1 + } + } + + when (sort) { + SORT_BY_NAME -> { + // sort by name + return asc * file1.title.compareTo(file2.title, ignoreCase = true) + } + SORT_BY_LAST_MODIFIED -> { + // sort by last modified + return asc * java.lang.Long.valueOf(file1.date).compareTo(file2.date) + } + SORT_BY_SIZE -> { + // sort by size + return if (!file1.isDirectory && !file2.isDirectory) { + asc * java.lang.Long.valueOf(file1.longSize).compareTo(file2.longSize) + } else { + file1.title.compareTo(file2.title, ignoreCase = true) + } + } + SORT_BY_TYPE -> { + // sort by type + return if (!file1.isDirectory && !file2.isDirectory) { + val ext_a = getExtension(file1.title) + val ext_b = getExtension(file2.title) + val res = asc * ext_a.compareTo(ext_b) + if (res == 0) { + asc * file1.title.compareTo(file2.title, ignoreCase = true) + } else { + res + } + } else { + file1.title.compareTo(file2.title, ignoreCase = true) + } + } + else -> return 0 + } + } + + companion object { + + const val SORT_BY_NAME = 0 + const val SORT_BY_LAST_MODIFIED = 1 + const val SORT_BY_SIZE = 2 + const val SORT_BY_TYPE = 3 + + const val SORT_DIR_ON_TOP = 0 + const val SORT_FILE_ON_TOP = 1 + const val SORT_NONE_ON_TOP = 2 + + const val SORT_ASC = 1 + const val SORT_DSC = -1 + + @Retention(AnnotationRetention.SOURCE) + @IntDef(SORT_BY_NAME, SORT_BY_LAST_MODIFIED, SORT_BY_SIZE, SORT_BY_TYPE) + annotation class SortBy + + @Retention(AnnotationRetention.SOURCE) + @IntDef(SORT_DIR_ON_TOP, SORT_FILE_ON_TOP, SORT_NONE_ON_TOP) + annotation class DirSortMode + + @Retention(AnnotationRetention.SOURCE) + @IntDef(SORT_ASC, SORT_DSC) + annotation class SortOrder + + /** + * Convenience method to get the file extension in given path. + * + * TODO: merge with same definition somewhere else (if any) + */ + @JvmStatic + fun getExtension(a: String): String { + return a.substringAfterLast('.').lowercase(Locale.getDefault()) + } + } +} 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..5dff098eb3 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,7 @@ import java.util.Date; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.Callable; +import java.util.Locale; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; @@ -42,7 +42,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 +70,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 +77,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 +89,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 +212,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. @@ -749,7 +674,7 @@ public static void openFile( mainActivity.startActivity(intent); } else { try { - openFileDialogFragmentFor(f, mainActivity); + openFileDialogFragmentFor(f, mainActivity, useNewStack); } catch (Exception e) { Toast.makeText( mainActivity, mainActivity.getString(R.string.no_app_found), Toast.LENGTH_LONG) @@ -760,30 +685,42 @@ public static void openFile( } private static void openFileDialogFragmentFor( - @NonNull File file, @NonNull MainActivity mainActivity) { + @NonNull File file, @NonNull MainActivity mainActivity, @NonNull Boolean useNewStack) { openFileDialogFragmentFor( - file, mainActivity, MimeTypes.getMimeType(file.getAbsolutePath(), false)); + file, mainActivity, MimeTypes.getMimeType(file.getAbsolutePath(), false), useNewStack); } private static void openFileDialogFragmentFor( - @NonNull File file, @NonNull MainActivity mainActivity, @NonNull String mimeType) { + @NonNull File file, + @NonNull MainActivity mainActivity, + @NonNull String mimeType, + @NonNull Boolean useNewStack) { OpenFileDialogFragment.Companion.openFileOrShow( FileProvider.getUriForFile(mainActivity, mainActivity.getPackageName(), file), mimeType, - false, + useNewStack, mainActivity, false); } private static void openFileDialogFragmentFor( - @NonNull DocumentFile file, @NonNull MainActivity mainActivity) { + @NonNull DocumentFile file, + @NonNull MainActivity mainActivity, + @NonNull Boolean useNewStack) { openFileDialogFragmentFor( - file.getUri(), mainActivity, MimeTypes.getMimeType(file.getUri().toString(), false)); + file.getUri(), + mainActivity, + MimeTypes.getMimeType(file.getUri().toString(), false), + useNewStack); } private static void openFileDialogFragmentFor( - @NonNull Uri uri, @NonNull MainActivity mainActivity, @NonNull String mimeType) { - OpenFileDialogFragment.Companion.openFileOrShow(uri, mimeType, false, mainActivity, false); + @NonNull Uri uri, + @NonNull MainActivity mainActivity, + @NonNull String mimeType, + @NonNull Boolean useNewStack) { + OpenFileDialogFragment.Companion.openFileOrShow( + uri, mimeType, useNewStack, mainActivity, false); } private static boolean isSelfDefault(File f, Context c) { @@ -804,7 +741,7 @@ public static void openFile( boolean useNewStack = sharedPrefs.getBoolean(PreferencesConstants.PREFERENCE_TEXTEDITOR_NEWSTACK, false); try { - openFileDialogFragmentFor(f, m); + openFileDialogFragmentFor(f, m, useNewStack); } catch (Exception e) { Toast.makeText(m, m.getString(R.string.no_app_found), Toast.LENGTH_LONG).show(); openWith(f, m, useNewStack); @@ -929,10 +866,19 @@ public static HybridFileParcelable parseName(String line, boolean isStat) { } link = new StringBuilder(link.toString().trim()); } - long Size = (size == null || size.trim().length() == 0) ? -1 : Long.parseLong(size); + long Size; + if (size == null || size.trim().length() == 0) { + Size = -1; + } else { + try { + Size = Long.parseLong(size); + } catch (NumberFormatException ifItIsNotANumber) { + Size = -1; + } + } if (date.trim().length() > 0 && !isStat) { ParsePosition pos = new ParsePosition(0); - SimpleDateFormat simpledateformat = new SimpleDateFormat("yyyy-MM-dd | HH:mm"); + SimpleDateFormat simpledateformat = new SimpleDateFormat("yyyy-MM-dd | HH:mm", Locale.US); Date stringDate = simpledateformat.parse(date, pos); if (stringDate == null) { LOG.warn("parseName: unable to parse datetime string [" + date + "]"); 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..4bed1f8397 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 @@ -243,10 +243,10 @@ private void startCopy( doCopy(inChannel, outChannel, updatePosition); } catch (IOException e) { - LOG.debug("I/O Error!", e); - throw new IOException(); + LOG.error("I/O Error copy {} to {}: {}", mSourceFile, mTargetFile, e); + throw new IOException(e); } catch (OutOfMemoryError e) { - LOG.warn("low memory while copying file", e); + LOG.warn("low memory while copying {} to {}: {}", mSourceFile, mTargetFile, e); onLowMemory.onLowMemory(); @@ -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..577c33199d --- /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 com.amaze.filemanager.filesystem.HybridFile +import org.slf4j.LoggerFactory + +object MediaConnectionUtils { + + private val LOG = LoggerFactory.getLogger(MediaConnectionUtils::class.java) + + /** + * 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.info("MediaConnectionUtils#scanFile finished scanning path$path") + } + } +} 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 e5fb262932..4cee083dff 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 @@ -70,7 +70,6 @@ object NetCopyClientConnectionPool { /** * Obtain a [NetCopyClient] connection from the underlying connection pool. * - * * Beneath it will return the connection if it exists; otherwise it will create a new one and * put it into the connection pool. * 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 3fedf1b0eb..61f3c85f5c 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 @@ -48,6 +48,10 @@ import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPReply import org.slf4j.LoggerFactory import java.io.IOException +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale object NetCopyClientUtils { @@ -74,7 +78,9 @@ object NetCopyClientUtils { * Execute the given NetCopyClientTemplate. * * This template pattern is borrowed from Spring Framework, to simplify code on operations - * using SftpClientTemplate. + * using NetCopyClientTemplate. + * + * FIXME: Over-simplification implementation causing unnecessarily closing SSHClient. * * @param template [NetCopyClientTemplate] to execute * @param Type of return value @@ -187,6 +193,7 @@ object NetCopyClientUtils { * @param fullUri Full SSH URL * @return The remote path part of the full SSH URL */ + @JvmStatic fun extractRemotePathFrom(fullUri: String): String { return NetCopyConnectionInfo(fullUri).let { connInfo -> if (true == connInfo.defaultPath?.isNotEmpty()) { @@ -291,4 +298,16 @@ object NetCopyClientUtils { SMB_URI_PREFIX -> 0 // SMB never requires explicit port number at URL else -> throw IllegalArgumentException("Cannot derive default port") } + + /** + * Convenience method to format given UNIX timestamp to yyyyMMddHHmmss format. + */ + @JvmStatic + fun getTimestampForTouch(date: Long): String { + val calendar = Calendar.getInstance() + calendar.timeInMillis = date + val df: DateFormat = SimpleDateFormat("yyyyMMddHHmmss", Locale.US) + df.calendar = calendar + return df.format(calendar.time) + } } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/root/MountPathCommand.kt b/app/src/main/java/com/amaze/filemanager/filesystem/root/MountPathCommand.kt index 6f7b44f602..9c26ba7641 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/root/MountPathCommand.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/root/MountPathCommand.kt @@ -22,6 +22,7 @@ package com.amaze.filemanager.filesystem.root import android.os.Build import com.amaze.filemanager.fileoperations.exceptions.ShellNotRunningException +import com.amaze.filemanager.filesystem.RootHelper import com.amaze.filemanager.filesystem.root.base.IRootCommand object MountPathCommand : IRootCommand() { @@ -38,7 +39,8 @@ object MountPathCommand : IRootCommand() { * @return String the root of mount point that was ro, and mounted to rw; null otherwise */ @Throws(ShellNotRunningException::class) - fun mountPath(path: String, operation: String): String? { + fun mountPath(pathArg: String, operation: String): String? { + val path = RootHelper.getCommandLineString(pathArg) return when (operation) { READ_WRITE -> mountReadWrite(path) READ_ONLY -> { @@ -96,7 +98,9 @@ object MountPathCommand : IRootCommand() { return if (mountOutput.isNotEmpty()) { // command failed, and we got a reason echo'ed null - } else mountPoint + } else { + mountPoint + } } } return null diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SFTPClientExt.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SFTPClientExt.kt new file mode 100644 index 0000000000..4f36e35996 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SFTPClientExt.kt @@ -0,0 +1,61 @@ +/* + * 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.filesystem.ssh + +import net.schmizz.sshj.sftp.FileAttributes +import net.schmizz.sshj.sftp.OpenMode +import net.schmizz.sshj.sftp.PacketType +import net.schmizz.sshj.sftp.RemoteFile +import net.schmizz.sshj.sftp.SFTPClient +import net.schmizz.sshj.sftp.SFTPEngine +import java.io.IOException +import java.util.EnumSet +import java.util.concurrent.TimeUnit + +const val READ_AHEAD_MAX_UNCONFIRMED_READS: Int = 16 + +/** + * Monkey-patch [SFTPEngine.open] until sshj adds back read ahead support in [RemoteFile]. + */ +@Throws(IOException::class) +fun SFTPEngine.openWithReadAheadSupport( + path: String, + modes: Set, + fa: FileAttributes +): RemoteFile { + val handle: ByteArray = request( + newRequest(PacketType.OPEN).putString(path, subsystem.remoteCharset) + .putUInt32(OpenMode.toMask(modes).toLong()).putFileAttributes(fa) + ).retrieve(timeoutMs.toLong(), TimeUnit.MILLISECONDS) + .ensurePacketTypeIs(PacketType.HANDLE).readBytes() + return RemoteFile(this, path, handle) +} + +/** + * Monkey-patch [SFTPClient.open] until sshj adds back read ahead support in [RemoteFile]. + */ +fun SFTPClient.openWithReadAheadSupport(path: String): RemoteFile { + return sftpEngine.openWithReadAheadSupport( + path, + EnumSet.of(OpenMode.READ), + FileAttributes.EMPTY + ) +} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.java deleted file mode 100644 index 8af4363fa0..0000000000 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.java +++ /dev/null @@ -1,227 +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.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.amaze.filemanager.R; -import com.amaze.filemanager.fileoperations.filesystem.cloud.CloudStreamer; -import com.amaze.filemanager.filesystem.HybridFile; -import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils; -import com.amaze.filemanager.ui.activities.MainActivity; -import com.amaze.filemanager.ui.icons.MimeTypes; - -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import net.schmizz.sshj.SSHClient; -import net.schmizz.sshj.connection.channel.direct.Session; -import net.schmizz.sshj.sftp.FileAttributes; -import net.schmizz.sshj.sftp.FileMode; -import net.schmizz.sshj.sftp.RemoteResourceInfo; -import net.schmizz.sshj.sftp.SFTPClient; - -public abstract class SshClientUtils { - - private static final Logger LOG = LoggerFactory.getLogger(SshClientUtils.class); - - /** - * Execute the given template with SshClientTemplate. - * - * @param template {@link SshClientSessionTemplate} to execute - * @param Type of return value - * @return Template execution results - */ - public static T execute(@NonNull final SshClientSessionTemplate template) { - return NetCopyClientUtils.INSTANCE.execute( - new SshClientTemplate(template.url, false) { - @Override - public T executeWithSSHClient(@NonNull SSHClient sshClient) { - Session session = null; - T retval = null; - try { - session = sshClient.startSession(); - retval = template.execute(session); - } catch (IOException e) { - LOG.error("Error executing template method", e); - } finally { - if (session != null && session.isOpen()) { - try { - session.close(); - } catch (IOException e) { - LOG.warn("Error closing SFTP client", e); - } - } - } - return retval; - } - }); - } - - /** - * Execute the given template with SshClientTemplate. - * - * @param template {@link SFtpClientTemplate} to execute - * @param Type of return value - * @return Template execution results - */ - @Nullable - public static T execute(@NonNull final SFtpClientTemplate template) { - final SshClientTemplate ftpClientTemplate = - new SshClientTemplate(template.url, false) { - @Override - @Nullable - public T executeWithSSHClient(SSHClient sshClient) { - SFTPClient sftpClient = null; - T retval = null; - try { - sftpClient = sshClient.newSFTPClient(); - retval = template.execute(sftpClient); - } catch (IOException e) { - LOG.error("Error executing template method", e); - } finally { - if (sftpClient != null && template.closeClientOnFinish) { - try { - sftpClient.close(); - } catch (IOException e) { - LOG.warn("Error closing SFTP client", e); - } - } - } - return retval; - } - }; - - return NetCopyClientUtils.INSTANCE.execute(ftpClientTemplate); - } - - /** - * Converts plain path smb://127.0.0.1/test.pdf to authorized path - * smb://test:123@127.0.0.1/test.pdf from server list - * - * @param path - * @return - */ - public static String formatPlainServerPathToAuthorised(ArrayList servers, String path) { - for (String[] serverEntry : servers) { - Uri inputUri = Uri.parse(path); - Uri serverUri = Uri.parse(serverEntry[1]); - if (inputUri.getScheme().equalsIgnoreCase(serverUri.getScheme()) - && serverUri.getAuthority().contains(inputUri.getAuthority())) { - String output = - inputUri - .buildUpon() - .encodedAuthority(serverUri.getEncodedAuthority()) - .build() - .toString(); - LOG.info("build authorised path {} from plain path {}", output, path); - return output; - } - } - return path; - } - - /** - * Disconnects the given {@link SSHClient} but wrap all exceptions beneath, so callers are free - * from the hassles of handling thrown exceptions. - * - * @param client {@link SSHClient} instance - */ - public static void tryDisconnect(SSHClient client) { - if (client != null && client.isConnected()) { - try { - client.disconnect(); - } catch (IOException e) { - LOG.warn("Error closing SSHClient connection", e); - } - } - } - - public static void launchFtp(final HybridFile baseFile, final MainActivity activity) { - final CloudStreamer streamer = CloudStreamer.getInstance(); - - new Thread( - () -> { - try { - boolean isDirectory = baseFile.isDirectory(activity); - long fileLength = baseFile.length(activity); - streamer.setStreamSrc( - baseFile.getInputStream(activity), baseFile.getName(activity), fileLength); - activity.runOnUiThread( - () -> { - try { - File file = - new File( - NetCopyClientUtils.INSTANCE.extractRemotePathFrom( - baseFile.getPath())); - Uri uri = - Uri.parse(CloudStreamer.URL + Uri.fromFile(file).getEncodedPath()); - Intent i = new Intent(Intent.ACTION_VIEW); - i.setDataAndType( - uri, MimeTypes.getMimeType(baseFile.getPath(), isDirectory)); - PackageManager packageManager = activity.getPackageManager(); - List resInfos = packageManager.queryIntentActivities(i, 0); - if (resInfos != null && resInfos.size() > 0) activity.startActivity(i); - else - Toast.makeText( - activity, - activity.getResources().getString(R.string.smb_launch_error), - Toast.LENGTH_SHORT) - .show(); - } catch (ActivityNotFoundException e) { - LOG.warn("failed to launch sftp file", e); - } - }); - } catch (Exception e) { - LOG.warn("failed to launch sftp file", e); - } - }) - .start(); - } - - public static boolean isDirectory(@NonNull SFTPClient client, @NonNull RemoteResourceInfo info) - throws IOException { - boolean isDirectory = info.isDirectory(); - if (info.getAttributes().getType().equals(FileMode.Type.SYMLINK)) { - try { - FileAttributes symlinkAttrs = client.stat(info.getPath()); - isDirectory = symlinkAttrs.getType().equals(FileMode.Type.DIRECTORY); - } catch (IOException ifSymlinkIsBroken) { - LOG.warn("Symbolic link {} is broken, skipping", info.getPath()); - throw ifSymlinkIsBroken; - } - } - return isDirectory; - } -} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.kt new file mode 100644 index 0000000000..12c8c40094 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshClientUtils.kt @@ -0,0 +1,222 @@ +/* + * 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.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import com.amaze.filemanager.R +import com.amaze.filemanager.fileoperations.filesystem.cloud.CloudStreamer +import com.amaze.filemanager.filesystem.HybridFile +import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils +import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils.extractRemotePathFrom +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.ui.icons.MimeTypes +import net.schmizz.sshj.SSHClient +import net.schmizz.sshj.connection.channel.direct.Session +import net.schmizz.sshj.sftp.FileMode +import net.schmizz.sshj.sftp.RemoteResourceInfo +import net.schmizz.sshj.sftp.SFTPClient +import org.slf4j.LoggerFactory +import java.io.File +import java.io.IOException +import kotlin.concurrent.thread + +object SshClientUtils { + + @JvmStatic + private val LOG = LoggerFactory.getLogger(SshClientUtils::class.java) + + @JvmField + val sftpGetSize: (String) -> Long? = { path -> + NetCopyClientUtils.execute(object : SFtpClientTemplate(path, true) { + override fun execute(client: SFTPClient): Long { + return client.size(extractRemotePathFrom(path)) + } + }) + } + + /** + * Execute the given template with SshClientTemplate. + * + * @param template [SshClientSessionTemplate] to execute + * @param Type of return value + * @return Template execution results + */ + @JvmStatic + fun execute(template: SshClientSessionTemplate): T? { + return NetCopyClientUtils.execute( + object : SshClientTemplate(template.url, false) { + override fun executeWithSSHClient(sshClient: SSHClient): T? { + var session: Session? = null + var retval: T? = null + try { + session = sshClient.startSession() + retval = template.execute(session) + } catch (e: IOException) { + LOG.error("Error executing template method", e) + } finally { + if (session != null && session.isOpen) { + try { + session.close() + } catch (e: IOException) { + LOG.warn("Error closing SFTP client", e) + } + } + } + return retval + } + } + ) + } + + /** + * Execute the given template with SshClientTemplate. + * + * @param template [SFtpClientTemplate] to execute + * @param Type of return value + * @return Template execution results + */ + @JvmStatic + fun execute(template: SFtpClientTemplate): T? { + return NetCopyClientUtils.execute(template) + } + + /** + * Converts plain path smb://127.0.0.1/test.pdf to authorized path + * smb://test:123@127.0.0.1/test.pdf from server list + * + * @param path + * @return + */ + @JvmStatic + fun formatPlainServerPathToAuthorised( + servers: ArrayList>, + path: String + ): String { + for (serverEntry in servers) { + val inputUri = Uri.parse(path) + val serverUri = Uri.parse(serverEntry[1]) + if (inputUri.scheme.equals(serverUri.scheme, ignoreCase = true) && + serverUri.authority!!.contains(inputUri.authority!!) + ) { + val output = inputUri + .buildUpon() + .encodedAuthority(serverUri.encodedAuthority) + .build() + .toString() + LOG.info("build authorised path {} from plain path {}", output, path) + return output + } + } + return path + } + + /** + * Disconnects the given [SSHClient] but wrap all exceptions beneath, so callers are free + * from the hassles of handling thrown exceptions. + * + * @param client [SSHClient] instance + */ + fun tryDisconnect(client: SSHClient?) { + if (client != null && client.isConnected) { + try { + client.disconnect() + } catch (e: IOException) { + LOG.warn("Error closing SSHClient connection", e) + } + } + } + + /** + * Open a remote SSH file on local Android device. It uses the [CloudStreamer] to stream the + * file. + */ + @JvmStatic + @Suppress("Detekt.TooGenericExceptionCaught") + fun launchFtp(baseFile: HybridFile, activity: MainActivity) { + val streamer = CloudStreamer.getInstance() + thread { + try { + val isDirectory = baseFile.isDirectory(activity) + val fileLength = baseFile.length(activity) + streamer.setStreamSrc( + baseFile.getInputStream(activity), + baseFile.getName(activity), + fileLength + ) + activity.runOnUiThread { + try { + val file = File( + extractRemotePathFrom( + baseFile.path + ) + ) + val uri = Uri.parse(CloudStreamer.URL + Uri.fromFile(file).encodedPath) + val i = Intent(Intent.ACTION_VIEW) + i.setDataAndType( + uri, + MimeTypes.getMimeType(baseFile.path, isDirectory) + ) + val packageManager = activity.packageManager + val resInfos = packageManager.queryIntentActivities(i, 0) + if (resInfos != null && resInfos.size > 0) { + activity.startActivity(i) + } else { + Toast.makeText( + activity, + activity.resources.getString(R.string.smb_launch_error), + Toast.LENGTH_SHORT + ) + .show() + } + } catch (e: ActivityNotFoundException) { + LOG.warn("failed to launch sftp file", e) + } + } + } catch (e: Exception) { + LOG.warn("failed to launch sftp file", e) + } + } + } + + /** + * Reads given [RemoteResourceInfo] and determines if the path it's related to is a directory. + * + * Will descend into corresponding target if given RemoteResourceInfo represents a symlink. + */ + @JvmStatic + @Throws(IOException::class) + fun isDirectory(client: SFTPClient, info: RemoteResourceInfo): Boolean { + var isDirectory = info.isDirectory + if (info.attributes.type == FileMode.Type.SYMLINK) { + try { + val symlinkAttrs = client.stat(info.path) + isDirectory = symlinkAttrs.type == FileMode.Type.DIRECTORY + } catch (ifSymlinkIsBroken: IOException) { + LOG.warn("Symbolic link {} is broken, skipping", info.path) + throw ifSymlinkIsBroken + } + } + return isDirectory + } +} 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/MainActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java index d899e0d7ac..391950311d 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 @@ -124,6 +124,7 @@ import com.amaze.filemanager.ui.fragments.ProcessViewerFragment; import com.amaze.filemanager.ui.fragments.SearchWorkerFragment; import com.amaze.filemanager.ui.fragments.TabFragment; +import com.amaze.filemanager.ui.fragments.data.MainFragmentViewModel; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.strings.StorageNamingHelper; import com.amaze.filemanager.ui.theme.AppTheme; @@ -142,7 +143,6 @@ import com.amaze.filemanager.utils.Utils; import com.cloudrail.si.CloudRail; import com.google.android.material.appbar.AppBarLayout; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; import com.leinardi.android.speeddial.FabWithLabelView; @@ -247,7 +247,7 @@ public class MainActivity extends PermissionsActivity public ArrayList oppatheList; // This holds the Uris to be written at initFabToSave() - private ArrayList urisToBeSaved; + private List urisToBeSaved; public static final String PASTEHELPER_BUNDLE = "pasteHelper"; @@ -324,7 +324,7 @@ public class MainActivity extends PermissionsActivity public static final int REQUEST_CODE_CLOUD_LIST_KEY = 5472; private PasteHelper pasteHelper; - private MainActivityActionMode mainActivityActionMode; + public MainActivityActionMode mainActivityActionMode; private static final String DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0"; private static final String INTERNAL_SHARED_STORAGE = "Internal shared storage"; @@ -634,9 +634,15 @@ private void checkForExternalIntent(Intent intent) { } else { // save a single file to filesystem Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - ArrayList uris = new ArrayList<>(); - uris.add(uri); - initFabToSave(uris); + if (uri != null + && uri.getScheme() != null + && uri.getScheme().startsWith(ContentResolver.SCHEME_FILE)) { + ArrayList uris = new ArrayList<>(); + uris.add(uri); + initFabToSave(uris); + } else { + Toast.makeText(this, R.string.error_unsupported_or_null_uri, Toast.LENGTH_LONG).show(); + } } // disable screen rotation just for convenience purpose // TODO: Support screen rotation when saving a file @@ -655,7 +661,7 @@ private void checkForExternalIntent(Intent intent) { } /** Initializes the floating action button to act as to save data from an external intent */ - private void initFabToSave(final ArrayList uris) { + private void initFabToSave(final List uris) { Utils.showThemedSnackbar( this, getString(R.string.select_save_location), @@ -664,7 +670,7 @@ private void initFabToSave(final ArrayList uris) { () -> saveExternalIntent(uris)); } - private void saveExternalIntent(final ArrayList uris) { + private void saveExternalIntent(final List uris) { executeWithMainFragment( mainFragment -> { if (uris != null && uris.size() > 0) { @@ -1789,7 +1795,34 @@ public void initializeFabActionViews() { FabWithLabelView newFolderFab = initFabTitle(R.id.menu_new_folder, R.string.folder, R.drawable.folder_fab); - floatingActionButton.setOnActionSelectedListener(new FabActionListener(this)); + floatingActionButton.setOnActionSelectedListener( + actionItem -> { + MainFragment mainFragment = getCurrentMainFragment(); + + if (mainFragment == null) return false; + + String path = mainFragment.getCurrentPath(); + + MainFragmentViewModel mainFragmentViewModel = mainFragment.getMainFragmentViewModel(); + + if (mainFragmentViewModel == null) return false; + + OpenMode openMode = mainFragmentViewModel.getOpenMode(); + + int id = actionItem.getId(); + + if (id == R.id.menu_new_folder) + mainActivity.mainActivityHelper.mkdir(openMode, path, mainFragment); + else if (id == R.id.menu_new_file) + mainActivity.mainActivityHelper.mkfile(openMode, path, mainFragment); + else if (id == R.id.menu_new_cloud) + new CloudSheetFragment() + .show(mainActivity.getSupportFragmentManager(), CloudSheetFragment.TAG_FRAGMENT); + + floatingActionButton.close(true); + return true; + }); + floatingActionButton.setOnClickListener( view -> { fabButtonClick(cloudFab); @@ -1992,6 +2025,7 @@ public void showSftpDialog(String name, String path, boolean edit) { if (i != -1) name = dataUtils.getServers().get(i)[0]; } SftpConnectDialog sftpConnectDialog = new SftpConnectDialog(); + sftpConnectDialog.setCancelable(false); String finalName = name; Flowable.fromCallable(() -> new NetCopyConnectionInfo(path)) .flatMap( @@ -2380,45 +2414,6 @@ private void initLeftRightAndTopDragListeners(boolean destroy, boolean shouldInv 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 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; - } - - floatingActionButton.close(true); - return true; - } - } /** * Invoke {@link FtpServerFragment#changeFTPServerPath(String)} to change FTP server share path. * 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..471661dea2 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 @@ -49,41 +49,37 @@ import com.amaze.filemanager.utils.Utils; import com.google.android.material.snackbar.Snackbar; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; import android.content.Context; -import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.Spanned; import android.text.TextWatcher; import android.text.style.BackgroundColorSpan; -import android.util.DisplayMetrics; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ViewAnimationUtils; -import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; 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.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.appcompat.widget.AppCompatImageButton; +import androidx.constraintlayout.widget.ConstraintLayout; 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; @@ -95,13 +91,14 @@ public class TextEditorActivity extends ThemedActivity private static final String KEY_ORIGINAL_TEXT = "original"; private static final String KEY_MONOFONT = "monofont"; - private RelativeLayout searchViewLayout; - public ImageButton upButton; - public ImageButton downButton; - public ImageButton closeButton; + private ConstraintLayout searchViewLayout; + public AppCompatImageButton upButton; + public AppCompatImageButton downButton; private Snackbar loadingSnackbar; + private TextEditorActivityViewModel viewModel; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -109,16 +106,15 @@ public void onCreate(Bundle savedInstanceState) { toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - final TextEditorActivityViewModel viewModel = - new ViewModelProvider(this).get(TextEditorActivityViewModel.class); + viewModel = new ViewModelProvider(this).get(TextEditorActivityViewModel.class); + + searchViewLayout = findViewById(R.id.textEditorSearchBar); - searchViewLayout = findViewById(R.id.searchview); searchViewLayout.setBackgroundColor(getPrimary()); - searchEditText = searchViewLayout.findViewById(R.id.search_box); - upButton = searchViewLayout.findViewById(R.id.prev); - downButton = searchViewLayout.findViewById(R.id.next); - closeButton = searchViewLayout.findViewById(R.id.close); + searchEditText = searchViewLayout.findViewById(R.id.textEditorSearchBox); + upButton = searchViewLayout.findViewById(R.id.textEditorSearchPrevButton); + downButton = searchViewLayout.findViewById(R.id.textEditorSearchNextButton); searchEditText.addTextChangedListener(this); @@ -126,14 +122,13 @@ public void onCreate(Bundle savedInstanceState) { // upButton.setEnabled(false); downButton.setOnClickListener(this); // downButton.setEnabled(false); - closeButton.setOnClickListener(this); - - boolean useNewStack = getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK); - getSupportActionBar().setDisplayHomeAsUpEnabled(!useNewStack); - - mainTextView = findViewById(R.id.fname); - scrollView = findViewById(R.id.editscroll); + if (getSupportActionBar() != null) { + boolean useNewStack = getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK); + getSupportActionBar().setDisplayHomeAsUpEnabled(!useNewStack); + } + mainTextView = findViewById(R.id.textEditorMainEditText); + scrollView = findViewById(R.id.textEditorScrollView); final Uri uri = getIntent().getData(); if (uri != null) { @@ -144,14 +139,23 @@ public void onCreate(Bundle savedInstanceState) { return; } - getSupportActionBar().setTitle(viewModel.getFile().name); + ActionBar actionBar = getSupportActionBar(); + + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(!getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK)); + actionBar.setTitle(viewModel.getFile().name); + } mainTextView.addTextChangedListener(this); if (getAppTheme().equals(AppTheme.DARK)) { - mainTextView.setBackgroundColor(Utils.getColor(this, R.color.holo_dark_background)); + mainTextView.setBackgroundColor(Utils.getColor(this, R.color.holo_dark_action_mode)); + mainTextView.setTextColor(Utils.getColor(this, R.color.primary_white)); } else if (getAppTheme().equals(AppTheme.BLACK)) { mainTextView.setBackgroundColor(Utils.getColor(this, android.R.color.black)); + mainTextView.setTextColor(Utils.getColor(this, R.color.primary_white)); + } else { + mainTextView.setTextColor(Utils.getColor(this, R.color.primary_grey_900)); } if (mainTextView.getTypeface() == null) { @@ -172,16 +176,17 @@ public void onCreate(Bundle savedInstanceState) { } else { load(this); } - initStatusBarResources(findViewById(R.id.texteditor)); + initStatusBarResources(findViewById(R.id.textEditorRootView)); } @Override - protected void onSaveInstanceState(Bundle outState) { + protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); final TextEditorActivityViewModel viewModel = new ViewModelProvider(this).get(TextEditorActivityViewModel.class); - outState.putString(KEY_MODIFIED_TEXT, mainTextView.getText().toString()); + outState.putString( + KEY_MODIFIED_TEXT, mainTextView.getText() != null ? mainTextView.getText().toString() : ""); outState.putInt(KEY_INDEX, mainTextView.getScrollY()); outState.putString(KEY_ORIGINAL_TEXT, viewModel.getOriginal()); outState.putBoolean(KEY_MONOFONT, inputTypefaceMono.equals(mainTextView.getTypeface())); @@ -193,6 +198,7 @@ private void checkUnsavedChanges() { if (viewModel.getOriginal() != null && mainTextView.isShown() + && mainTextView.getText() != null && !viewModel.getOriginal().equals(mainTextView.getText().toString())) { new MaterialDialog.Builder(this) .title(R.string.unsaved_changes) @@ -293,10 +299,13 @@ public boolean onOptionsItemSelected(MenuItem item) { break; case R.id.save: // Make sure EditText is visible before saving! - saveFile(this, mainTextView.getText().toString()); + if (mainTextView.getText() != null) { + saveFile(this, mainTextView.getText().toString()); + } break; case R.id.details: if (editableFileAbstraction.scheme.equals(FILE) + && editableFileAbstraction.hybridFileParcelable.getFile() != null && editableFileAbstraction.hybridFileParcelable.getFile().exists()) { GeneralDialogCreation.showPropertiesDialogWithoutPermissions( editableFileAbstraction.hybridFileParcelable, this, getAppTheme()); @@ -316,7 +325,7 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.openwith: if (editableFileAbstraction.scheme.equals(FILE)) { File currentFile = editableFileAbstraction.hybridFileParcelable.getFile(); - if (currentFile.exists()) { + if (currentFile != null && currentFile.exists()) { boolean useNewStack = getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK); FileUtils.openWith(currentFile, this, useNewStack); } else { @@ -355,7 +364,8 @@ public void onDestroy() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { // condition to check if callback is called in search editText - if (searchEditText != null && charSequence.hashCode() == searchEditText.getText().hashCode()) { + if (searchEditText.getText() != null + && charSequence.hashCode() == searchEditText.getText().hashCode()) { final TextEditorActivityViewModel viewModel = new ViewModelProvider(this).get(TextEditorActivityViewModel.class); @@ -371,7 +381,8 @@ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - if (charSequence.hashCode() == mainTextView.getText().hashCode()) { + if (mainTextView.getText() != null + && charSequence.hashCode() == mainTextView.getText().hashCode()) { final TextEditorActivityViewModel viewModel = new ViewModelProvider(this).get(TextEditorActivityViewModel.class); final Timer oldTimer = viewModel.getTimer(); @@ -400,11 +411,12 @@ public void run() { new ViewModelProvider(textEditorActivity).get(TextEditorActivityViewModel.class); modified = - !textEditorActivity - .mainTextView - .getText() - .toString() - .equals(viewModel.getOriginal()); + textEditorActivity.mainTextView.getText() != null + && !textEditorActivity + .mainTextView + .getText() + .toString() + .equals(viewModel.getOriginal()); if (viewModel.getModified() != modified) { viewModel.setModified(modified); invalidateOptionsMenu(); @@ -420,7 +432,8 @@ public void run() { @Override public void afterTextChanged(Editable editable) { // searchBox callback block - if (searchEditText != null && editable.hashCode() == searchEditText.getText().hashCode()) { + if (searchEditText.getText() != null + && editable.hashCode() == searchEditText.getText().hashCode()) { final WeakReference textEditorActivityWR = new WeakReference<>(this); final OnProgressUpdate onProgressUpdate = @@ -429,7 +442,7 @@ public void afterTextChanged(Editable editable) { if (textEditorActivity == null) { return; } - textEditorActivity.unhighlightSearchResult(index); + textEditorActivity.colorSearchResult(index, getPrimary()); }; final OnAsyncTaskFinished> onAsyncTaskFinished = @@ -445,7 +458,7 @@ public void afterTextChanged(Editable editable) { viewModel.setSearchResultIndices(data); for (SearchResultIndex searchResultIndex : data) { - textEditorActivity.unhighlightSearchResult(searchResultIndex); + textEditorActivity.colorSearchResult(searchResultIndex, getPrimary()); } if (data.size() != 0) { @@ -460,87 +473,72 @@ public void afterTextChanged(Editable editable) { } }; - searchTextTask = - new SearchTextTask( - mainTextView.getText().toString(), - editable.toString(), - onProgressUpdate, - onAsyncTaskFinished); - searchTextTask.execute(); + if (mainTextView.getText() != null) { + searchTextTask = + new SearchTextTask( + mainTextView.getText().toString(), + editable.toString(), + onProgressUpdate, + onAsyncTaskFinished); + searchTextTask.execute(); + } } } - /** show search view with a circular reveal animation */ private void revealSearchView() { - int startRadius = 4; - int endRadius = Math.max(searchViewLayout.getWidth(), searchViewLayout.getHeight()); - DisplayMetrics metrics = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(metrics); + searchViewLayout.setVisibility(View.VISIBLE); - // hardcoded and completely random - int cx = metrics.widthPixels - 160; - int cy = toolbar.getBottom(); - Animator animator; + Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_in_top); - // FIXME: 2016/11/18 ViewAnimationUtils Compatibility - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - animator = - ViewAnimationUtils.createCircularReveal(searchViewLayout, cx, cy, startRadius, endRadius); - else animator = ObjectAnimator.ofFloat(searchViewLayout, "alpha", 0f, 1f); + animation.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} - animator.setInterpolator(new AccelerateDecelerateInterpolator()); - animator.setDuration(600); - searchViewLayout.setVisibility(View.VISIBLE); - searchEditText.setText(""); - animator.start(); - animator.addListener( - new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(Animation animation) { + searchEditText.requestFocus(); - InputMethodManager imm = - (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); + + ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)) + .showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); } + + @Override + public void onAnimationRepeat(Animation animation) {} }); + + searchViewLayout.startAnimation(animation); } - /** hide search view with a circular reveal animation */ private void hideSearchView() { - int endRadius = 4; - int startRadius = Math.max(searchViewLayout.getWidth(), searchViewLayout.getHeight()); - - DisplayMetrics metrics = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(metrics); - // hardcoded and completely random - int cx = metrics.widthPixels - 160; - int cy = toolbar.getBottom(); + Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out_top); - Animator animator; - // FIXME: 2016/11/18 ViewAnimationUtils Compatibility - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - animator = - ViewAnimationUtils.createCircularReveal(searchViewLayout, cx, cy, startRadius, endRadius); - } else { - animator = ObjectAnimator.ofFloat(searchViewLayout, "alpha", 0f, 1f); - } + animation.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} - animator.setInterpolator(new AccelerateDecelerateInterpolator()); - animator.setDuration(600); - animator.start(); - animator.addListener( - new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(Animation animation) { + searchViewLayout.setVisibility(View.GONE); - InputMethodManager inputMethodManager = - (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow( - searchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); + + cleanSpans(viewModel); + searchEditText.setText(""); + + ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)) + .hideSoftInputFromWindow( + searchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); } + + @Override + public void onAnimationRepeat(Animation animation) {} }); + + searchViewLayout.startAnimation(animation); } @Override @@ -549,7 +547,7 @@ public void onClick(View v) { new ViewModelProvider(this).get(TextEditorActivityViewModel.class); switch (v.getId()) { - case R.id.prev: + case R.id.textEditorSearchPrevButton: // upButton if (viewModel.getCurrent() > 0) { unhighlightCurrentSearchResult(viewModel); @@ -560,7 +558,7 @@ public void onClick(View v) { highlightCurrentSearchResult(viewModel); } break; - case R.id.next: + case R.id.textEditorSearchNextButton: // downButton if (viewModel.getCurrent() < viewModel.getSearchResultIndices().size() - 1) { unhighlightCurrentSearchResult(viewModel); @@ -570,11 +568,6 @@ public void onClick(View v) { highlightCurrentSearchResult(viewModel); } break; - case R.id.close: - // closeButton - findViewById(R.id.searchview).setVisibility(View.GONE); - cleanSpans(viewModel); - break; default: throw new IllegalStateException(); } @@ -586,41 +579,34 @@ private void unhighlightCurrentSearchResult(final TextEditorActivityViewModel vi } SearchResultIndex resultIndex = viewModel.getSearchResultIndices().get(viewModel.getCurrent()); - unhighlightSearchResult(resultIndex); + colorSearchResult(resultIndex, getPrimary()); } private void highlightCurrentSearchResult(final TextEditorActivityViewModel viewModel) { SearchResultIndex keyValueNew = viewModel.getSearchResultIndices().get(viewModel.getCurrent()); - colorSearchResult(keyValueNew, Utils.getColor(this, R.color.search_text_highlight)); + colorSearchResult(keyValueNew, getAccent()); // scrolling to the highlighted element - scrollView.scrollTo( - 0, - (Integer) keyValueNew.getLineNumber() - + mainTextView.getLineHeight() - + Math.round(mainTextView.getLineSpacingExtra()) - - getSupportActionBar().getHeight()); - } - - private void unhighlightSearchResult(SearchResultIndex resultIndex) { - @ColorInt int color; - if (getAppTheme().equals(AppTheme.LIGHT)) { - color = Color.YELLOW; - } else { - color = Color.LTGRAY; + if (getSupportActionBar() != null) { + scrollView.scrollTo( + 0, + (Integer) keyValueNew.getLineNumber() + + mainTextView.getLineHeight() + + Math.round(mainTextView.getLineSpacingExtra()) + - getSupportActionBar().getHeight()); } - - colorSearchResult(resultIndex, color); } private void colorSearchResult(SearchResultIndex resultIndex, @ColorInt int color) { - mainTextView - .getText() - .setSpan( - new BackgroundColorSpan(color), - (Integer) resultIndex.getStartCharNumber(), - (Integer) resultIndex.getEndCharNumber(), - Spanned.SPAN_INCLUSIVE_INCLUSIVE); + if (mainTextView.getText() != null) { + mainTextView + .getText() + .setSpan( + new BackgroundColorSpan(color), + (Integer) resultIndex.getStartCharNumber(), + (Integer) resultIndex.getEndCharNumber(), + Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } } private void cleanSpans(TextEditorActivityViewModel viewModel) { @@ -630,10 +616,12 @@ private void cleanSpans(TextEditorActivityViewModel viewModel) { viewModel.setLine(0); // clearing textView spans - BackgroundColorSpan[] colorSpans = - mainTextView.getText().getSpans(0, mainTextView.length(), BackgroundColorSpan.class); - for (BackgroundColorSpan colorSpan : colorSpans) { - mainTextView.getText().removeSpan(colorSpan); + if (mainTextView.getText() != null) { + BackgroundColorSpan[] colorSpans = + mainTextView.getText().getSpans(0, mainTextView.length(), BackgroundColorSpan.class); + for (BackgroundColorSpan colorSpan : colorSpans) { + mainTextView.getText().removeSpan(colorSpan); + } } } } 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..5f6db7fd8e 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,11 @@ 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.AppCompatButton; +import androidx.appcompat.widget.AppCompatTextView; import androidx.core.util.Pair; import androidx.preference.Preference.BaseSavedState; import androidx.preference.PreferenceDialogFragmentCompat; @@ -167,7 +168,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 +186,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 +198,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 +250,9 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { ((UserColorPreferences) requireArguments().getParcelable(ARG_COLOR_PREF)).getAccent(); // Button views - ((TextView) dialog.findViewById(res.getIdentifier("button1", "id", "android"))) + ((AppCompatButton) dialog.findViewById(res.getIdentifier("button1", "id", "android"))) .setTextColor(accentColor); - ((TextView) dialog.findViewById(res.getIdentifier("button2", "id", "android"))) + ((AppCompatButton) 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