Kevin Marlow

Mobile Engineer

Engineering Manager at Sentio. We turn Android smartphones into work computers.

Efficient Multi-directory File Searching in Android

In this post, we will talk about an efficient multi-directory file search in Android using RxJava 2.

The general idea behind the searching algorithm is to run the search across available directories, emitting on each find. The results are then merged back into a single stream and delivered to the UI or caller.

Just want the source code? Check it out here.

First things first

In order to do any file searching, you will need to request the following permission in your AndroidManifest.xml. This lets you read the file system.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />  

You will also need the latest version of RxJava 2, RxAndroid, and RxBinding.

Setting up your Activity

Our sample app will implement a simple Model-View-Presenter architecture. This enables us to test our business logic and make sure everything runs smoothly.

Let's add a few things to our Activity.

  1. An EditText
  2. Our presenter
  3. A screen interface
  4. A RecyclerView to show the results
  5. A TextView to show file found count

Here is what our activity_main.xml looks like now.

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android=""  

        tools:visibility="visible" />

        android:hint="@string/search_hint" />

        tools:listitem="@layout/row_item_search_result" />


Here is our Screen and our Presenter.

public interface MainScreen {  
    void updateFileList(List<FileItemViewModel> fileItemViewModels);

    void requestPermissions(String[] strings, int requestCode);

public class MainPresenter {

    private final MainScreen screen;
    private final SearchEngine searchEngine;
    private final ThreadSchedulers threadSchedulers;
    private final PermissionManager permissionManager;
    private Disposable searchDisposable;

    public MainPresenter(MainScreen screen, SearchEngine searchEngine, ThreadSchedulers threadSchedulers, PermissionManager permissionManager) {
        this.screen = screen;
        this.searchEngine = searchEngine;
        this.threadSchedulers = threadSchedulers;
        this.permissionManager = permissionManager;

    public void onSearchTextChanges(InitialValueObservable<TextViewAfterTextChangeEvent> textChangeObservable) {

    public void onDestroy() {


They are added to the Activity like so.

public class MainActivity extends AppCompatActivity implements MainScreen {

    protected void onCreate(Bundle savedInstanceState) {


    private void setupViews() {
        tvSearchInput = findViewById(;
        tvFound = findViewById(;
        rvSearchResults = findViewById(;

    private void setupPresenter() {
        ThreadSchedulers threadSchedulers = new ThreadSchedulers();
        DirectoryRepo directoryRepo = new DirectoryRepo(this);
        SearchEngine searchEngine = new SearchEngine(directoryRepo, threadSchedulers);
        PermissionManager permissionManager = new PermissionManager(this);
        presenter = new MainPresenter(this, searchEngine, threadSchedulers, permissionManager);
    }private void setupViewObservables() {

    private void setupRecyclerView() {
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        rvSearchResults.setItemAnimator(new DefaultItemAnimator());

        searchResultsAdapter = new SearchResultsAdapter(this);

Listening to Text Changes

The best way to implement a search of any kind, whether it be Files, an online API, or a database lookup, is to watch when the text changes in the input field and respond. RxAndroid makes this very easy for us.

Let's get our view and add an AfterTextChangeObservable.

We then pass it to our presenter so that our presenter can run our business logic.

    private void setupViewObservables() {
Debounce for UX

With any good search, you will need to filter the input. In our case, we also want to debounce it. This way, when the user is typing really quickly, we won't start searching until the typing has "settled".

In the presenter, we will take our afterTextChangeEvents observable, and filter like so.

        .debounce(200, TimeUnit.MILLISECONDS)
        .subscribe(keyword -> {
            if (keyword.isEmpty()) {
                Timber.d("Search field is empty. Clearing screen");
            } else {
                Timber.d("Beginning search for '%s'", keyword);
        }, Timber::e));

Note that if the search input "settles" on empty, we do want to cancel any previously running search and clear the screen.

Split and Search

When we get a settled string to search, we call into our search engine to start getting the results. These results are batched by SEARCH_RESULT_BATCH_TIME milliseconds using RxJava's buffer.

When a keyword is entered, we fetch the directories to search, then flatMap across a custom Flowable that we create. Those results are batched into small listed and returned down the chain. The Presenter then maps the results into our view models and accumulates them into one sorted list, which is delivered to the UI for displaying.

    private void search(String keyword) {
        searchDisposable = searchEngine.startSearchDirectory(keyword)
                .scan(new ArrayList<>(), accumulateSortList(comparator()))
                .subscribe(screen::updateFileList, Timber::e);

    private void disposeSearch() {
        if (searchDisposable != null && !searchDisposable.isDisposed()) {

The SearchEngine creates a new Flowable emitter for each directory to search. Each time our matcher finds a match, it will emit the resulting file down the chain and into the buffer. Our example matcher is fairly rudimentary and just does a case-insensitive name match, but it could easily be replaced by a more complex matching mechanism.

public class SearchEngine {

    public Flowable<List<File>> startSearchDirectory(final String keyword) {
        return Flowable.fromIterable(directoryRepo.directoriesToSearch())
                .flatMap(directory -> searchDirectory(directory, keyword))
                .buffer(SEARCH_RESULT_BATCH_TIME, TimeUnit.MILLISECONDS);

    private Flowable<File> searchDirectory(final File folderDirectory, final String keyword) {
        return Flowable.create((FlowableOnSubscribe<File>) emitter -> {
            searchDirectory(folderDirectory, keyword, emitter);
        }, BackpressureStrategy.BUFFER)

    private void searchDirectory(File directory, String keyword,
                                 final FlowableEmitter<File> emitter) {
        final Queue<File> directories = new ArrayDeque<>(100);

        while (!directories.isEmpty()) {
            File dir = directories.poll();
            File[] files = getFilesFromDirectory(dir, keyword);
            if (files != null && files.length > 0) {

                for (File file : files) {
                    if (isVisibleFile(file)) {
                    } else if (isDirectoryAndCanRead(file)) {


    private File[] getFilesFromDirectory(File directory, final String keyword) {
        File[] files;
        try {
            FileFilter filter = file -> file.isDirectory() || file.getName().toLowerCase(Locale.getDefault())
                    .matches(MATCH_ALL + keyword.toLowerCase(Locale.getDefault()) + MATCH_ALL);

            files = directory.listFiles(filter);
        } catch (PatternSyntaxException e) {
            files = new File[0];
        return files;
Deliver Results

Finally, for every emission from the buffer, we will show the results on screen. Since the buffer is a timed emission, this results in a built up list of results, which gives the user a visual indication of the powerful search engine at work. All we need to do is update the adapter and our TextView with the resulting view models inside of our MainActivity.

    public void updateFileList(List<FileItemViewModel> fileItemViewModels) {
        tvFound.setVisibility(fileItemViewModels.size() == 0 ? View.INVISIBLE : View.VISIBLE);
        tvFound.setText(getString(R.string.found_x, fileItemViewModels.size()));

With a few smart tricks, we are able to efficiently search across multiple directories and incrementally show the results to the user as they are found. This technique can easily be adapted to perform a mixed data lookup, for example an API call, a database call, and a file directory call all in one.

Want the source code? Check it out here.