Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (C) 2014-2026 Arpit Khurana <arpitkh96@gmail.com>, Vishal Nehra <vishalmeham2@gmail.com>,
* Emmanuel Messulam<emmanuelbendavid@gmail.com>, Raymond Lai <airwave209gt at gmail.com> 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 <http://www.gnu.org/licenses/>.
*/

package com.amaze.filemanager.ui.activities

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.LargeTest
import com.amaze.filemanager.fileoperations.filesystem.OpenMode
import com.amaze.filemanager.filesystem.HybridFile
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.io.File

/**
* Instrumented (emulator/device) test verifying that [MainActivity.teleportToFile]
* actually scrolls the target file into view within the real file-list RecyclerView,
* on top of the headless assertions in [MainActivityTeleportTest].
*
* Uses plain Espresso view matching (checking the target file's name is displayed on
* screen) rather than accessing MainFragment's `listView`/`adapter` fields directly,
* since those are private -- this keeps the test decoupled from internal implementation
* details, following the same style as the existing TextEditorActivityEspressoTest.
*/
@LargeTest
class MainActivityTeleportScrollInstrumentedTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)

private lateinit var testDir: File
private lateinit var targetFile: File

/**
* Creates a temp directory (inside the app's own external files dir, which needs no
* runtime permission) with enough files that the target file starts off-screen and a
* real scroll is required to bring it into view.
*/
@Before
fun setUp() {
// Use the shared storage root (NOT getExternalFilesDir / Android/data), since Amaze
// treats any path under Android/data specially (Scoped Storage) and prompts for SAF
// access before browsing there, even for its own app-specific folder. The shared
// root instead relies on MANAGE_EXTERNAL_STORAGE, which must be pre-granted via:
// adb shell appops set <applicationId> MANAGE_EXTERNAL_STORAGE allow
testDir = File(android.os.Environment.getExternalStorageDirectory(), "AmazeTeleportScrollTest")
val created = testDir.mkdirs()
check(created || testDir.isDirectory) {
"Failed to create test directory at ${testDir.absolutePath} " +
"(mkdirs() returned $created, exists=${testDir.exists()}, isDirectory=${testDir.isDirectory}). " +
"Did you run: adb shell appops set <applicationId> MANAGE_EXTERNAL_STORAGE allow ?"
}

for (i in 1..40) {
val file = File(testDir, "file_%02d.txt".format(i))
check(file.createNewFile() || file.exists()) {
"Failed to create ${file.absolutePath} - parent exists: ${testDir.exists()}"
}
}
targetFile = File(testDir, "file_40.txt")
}

/**
* Removes the temp test directory after the test finishes.
*/
@After
fun tearDown() {
if (::testDir.isInitialized) {
testDir.deleteRecursively()
}
}

/**
* Verifies the target file's row becomes visible on screen after teleportToFile is called.
*/
@Test
fun testTeleportScrollsTargetFileIntoView() {
activityRule.scenario.onActivity { activity ->
val file = HybridFile(OpenMode.FILE, targetFile.absolutePath)
activity.teleportToFile(file)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an emulator test, run as much as possible via the UI, in this case, run a search and press the teleport to file button. You might want to implement this as a Roboletric test, but that is your choice..

}

// Give the async directory load + scroll a moment to complete before asserting.
Thread.sleep(2000)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No sleeps, use event based callbacks, unless it is a hack, which should be documented. Also, the emulator should not have animations of any kind when running instrumented tests, per docs.


onView(withText(targetFile.name))
.check(matches(isDisplayed()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
package com.amaze.filemanager.adapters

import android.content.Context
import android.graphics.PorterDuff
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
Expand Down Expand Up @@ -91,24 +93,29 @@ class SearchRecyclerViewAdapter :
holder.filePathTV.text = file.path.substring(0, file.path.lastIndexOf("/"))

holder.colorView.setBackgroundColor(getRandomColor(holder.colorView.context))
holder.teleportIV.setColorFilter(colorPreference.accent, PorterDuff.Mode.SRC_ATOP)

if (file.isDirectory) {
holder.colorView.setBackgroundColor(colorPreference.primaryFirstTab)
holder.teleportIV.visibility = View.GONE
} else {
holder.colorView.setBackgroundColor(colorPreference.accent)
holder.teleportIV.visibility = View.VISIBLE
}
}

inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val fileNameTV: AppCompatTextView
val filePathTV: AppCompatTextView
val colorView: View
val teleportIV: AppCompatImageView

init {

fileNameTV = view.findViewById(R.id.searchItemFileNameTV)
filePathTV = view.findViewById(R.id.searchItemFilePathTV)
colorView = view.findViewById(R.id.searchItemSampleColorView)
teleportIV = view.findViewById(R.id.searchItemTeleportIV)

view.setOnClickListener {

Expand All @@ -127,6 +134,14 @@ class SearchRecyclerViewAdapter :
(AppConfig.getInstance().mainActivityContext as MainActivity?)
?.appbar?.searchView?.hideSearchView()
}
teleportIV.setOnClickListener {
val (file, _) = getItem(adapterPosition)
if (!file.isDirectory) {
val activity = AppConfig.getInstance().mainActivityContext as MainActivity?
activity?.teleportToFile(file)
activity?.appbar?.searchView?.hideSearchView()
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,17 @@ public void goToMain(String path, boolean hideFab) {
}
}

public void teleportToFile(HybridFile file) {
String parentPath = file.getParent(this);
if (parentPath == null) {
scrollToFileName = null;
goToMain(file.getPath());
return;
}
scrollToFileName = file.getName(this);
goToMain(parentPath);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater menuInflater = getMenuInflater();
Expand Down Expand Up @@ -2456,11 +2467,11 @@ private void initLeftRightAndTopDragListeners(boolean destroy, boolean shouldInv
/**
* Invoke {@link FtpServerFragment#changeFTPServerPath(String)} to change FTP server share path.
*
* @param dialog

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't reorder lines to leave @see below other stuff.

* @param folder selected folder
* @see FtpServerFragment#changeFTPServerPath(String)
* @see FolderChooserDialog
* @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback
* @param dialog
* @param folder selected folder
*/
@Override
public void onFolderSelection(@NonNull FolderChooserDialog dialog, @NonNull File folder) {
Expand Down Expand Up @@ -2540,6 +2551,10 @@ public String getScrollToFileName() {
return this.scrollToFileName;
}

public void setScrollToFileName(String scrollToFileName) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add @Nullable to the type.

this.scrollToFileName = scrollToFileName;
}

/**
* Set list item selected value
*
Expand All @@ -2552,8 +2567,8 @@ public void setListItemSelected(boolean value) {
/**
* Do nothing other than dismissing the folder selection dialog.
*
* @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback
* @param dialog
* @see com.afollestad.materialdialogs.folderselector.FolderChooserDialog.FolderCallback

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't reorder lines to leave @see below other stuff.

*/
@Override
public void onFolderChooserDismissed(@NonNull FolderChooserDialog dialog) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -964,18 +964,14 @@ public void reloadListElements(boolean back, boolean grid) {
if (!isAdded()) return;

String fileName = requireMainActivity().getScrollToFileName();

if (fileName != null)
mainFragmentViewModel
.getScrollPosition(fileName)
.observe(
getViewLifecycleOwner(),
scrollPosition -> {
if (scrollPosition != -1)
listView.scrollToPosition(
Math.min(scrollPosition + 4, adapter.getItemCount() - 1));
adapter.notifyItemChanged(scrollPosition);
});
if (fileName != null) {
int scrollPosition = mainFragmentViewModel.getScrollPosition(fileName);
if (scrollPosition != -1) {
requireMainActivity().setScrollToFileName(null);
listView.scrollToPosition(Math.min(scrollPosition + 4, adapter.getItemCount() - 1));
adapter.notifyItemChanged(scrollPosition);
}
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,20 +228,14 @@ class MainFragmentViewModel : ViewModel() {
/**
* Get the position of an item
*/
fun getScrollPosition(title: String): MutableLiveData<Int> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this changed? Doesn't it have performance implications?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was having a bug in teleporting to a file already in the currently open folder didn't scroll or highlight anything. Root cause is that reloadListElements can fire more than once per navigation — once with a partial list while the folder is still loading, once with the full list once it's done. scrollToFileName was being cleared right after the first call read it, regardless of whether the file was actually found. So by the time the second, complete pass ran, the target filename was already gone and nothing happened.

i fixed it by moving setScrollToFileName(null) to only run after scrollPosition != -1

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While fixing that I also noticed getScrollPosition itself had a separate issue: it returned a MutableLiveData<Int> initialized to -1, then launched a coroutine on Dispatchers.IO to search adapterListItems and postValue the real index once found. That's two values arriving at two different times with no guarantee about when the second one lands relative to anything else reading it is a race condition. And there's no real reason for it
adapterListItems is just an in-memory list, there's no I/O to justify running the search on a background thread at all.
i fixed it by make getScrollPosition synchronous so it returns one correct value immediately,

val mutableLiveData: MutableLiveData<Int> = MutableLiveData(-1)

viewModelScope.launch(Dispatchers.IO) {
adapterListItems?.forEachIndexed { index, item ->
if (item.layoutElementParcelable != null &&
item.layoutElementParcelable?.title.equals(title)
) {
item.setChecked(true)
mutableLiveData.postValue(index)
}
fun getScrollPosition(title: String): Int {
adapterListItems?.forEachIndexed { index, item ->
if (item.layoutElementParcelable?.title == title) {
item.setChecked(true)
return index
}
}
return mutableLiveData
return -1
}

/**
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_folder_arrow_right_outline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- drawable/ic_folder_arrow_right_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M13 19C13 19.34 13.04 19.67 13.09 20H4C2.9 20 2 19.11 2 18V6C2 4.89 2.89 4 4 4H10L12 6H20C21.1 6 22 6.89 22 8V13.81C21.39 13.46 20.72 13.22 20 13.09V8H4V18H13.09C13.04 18.33 13 18.66 13 19M23 19L20 16V18H16V20H20V22L23 19Z" /></vector>
15 changes: 13 additions & 2 deletions app/src/main/res/layout/search_row_item.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/searchItemTeleportIV"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
android:padding="2dp"
app:srcCompat="@drawable/ic_folder_arrow_right_outline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/searchItemFileNameTV"
android:layout_width="0dp"
Expand All @@ -30,7 +41,7 @@
android:layout_marginBottom="2dp"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/searchItemFilePathTV"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/searchItemTeleportIV"
app:layout_constraintStart_toEndOf="@id/searchItemSampleColorView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0" />
Expand All @@ -46,7 +57,7 @@
android:letterSpacing="0.05"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/searchItemTeleportIV"
app:layout_constraintStart_toEndOf="@id/searchItemSampleColorView"
app:layout_constraintTop_toBottomOf="@id/searchItemFileNameTV"
app:layout_constraintVertical_bias="0" />
Expand Down
Loading
Loading