<commit_before>package org.openintents.filemanager.lists;

import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.test.espresso.IdlingResource;
import android.support.v4.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewFlipper;

import org.openintents.filemanager.FileHolderListAdapter;
import org.openintents.filemanager.FileManagerApplication;
import org.openintents.filemanager.R;
import org.openintents.filemanager.compatibility.ActionbarRefreshHelper;
import org.openintents.filemanager.files.DirectoryContents;
import org.openintents.filemanager.files.DirectoryScanner;
import org.openintents.filemanager.files.FileHolder;
import org.openintents.filemanager.util.CopyHelper;
import org.openintents.filemanager.util.MimeTypes;
import org.openintents.intents.FileManagerIntents;

import java.io.File;
import java.util.ArrayList;

import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.support.v4.content.ContextCompat.checkSelfPermission;


/**
 * A {@link ListFragment} that displays the contents of a directory.
 * <p>
 * Clicks do nothing.
 * </p>
 * <p>
 * Refreshes OnSharedPreferenceChange
 * </p>
 *
 * @author <NAME>
 */
public abstract class FileListFragment extends ListFragment {
    private static final String INSTANCE_STATE_PATH = "path";
    private static final String INSTANCE_STATE_FILES = "files";
    private static final int REQUEST_CODE_STORAGE_PERMISSION = 1;
    File mPreviousDirectory = null;

    // Not an anonymous inner class because of:
    // http://stackoverflow.com/questions/2542938/sharedpreferences-onsharedpreferencechangelistener-not-being-called-consistently
    private OnSharedPreferenceChangeListener preferenceListener = new OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(
                SharedPreferences sharedPreferences, String key) {
            // We only care for list-altering preferences. This could be
            // dangerous though,
            // as later contributors might not see this, and have their settings
            // not work in realtime.
            // Therefore this is commented out, since it's not likely the
            // refresh is THAT heavy.
            // *****************
            // if (PreferenceActivity.PREFS_DISPLAYHIDDENFILES.equals(key)
            // || PreferenceActivity.PREFS_SORTBY.equals(key)
            // || PreferenceActivity.PREFS_ASCENDING.equals(key))

            // Prevent NullPointerException caused from this getting called
            // after we have finish()ed the activity.
            if (getActivity() != null)
                refresh();
        }
    };

    protected FileHolderListAdapter mAdapter;
    protected DirectoryScanner mScanner;
    protected ArrayList<FileHolder> mFiles = new ArrayList<>();
    private String mPath;
    private String mFilename;

    private ViewFlipper mFlipper;
    private File mCurrentDirectory;
    private View mClipboardInfo;
    private TextView mClipboardContent;
    private TextView mClipboardAction;
    private IdlingResource.ResourceCallback resourceCallback;

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putString(INSTANCE_STATE_PATH, mPath);
        outState.putParcelableArrayList(INSTANCE_STATE_FILES, mFiles);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.filelist, null);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {

        // Set auto refresh on preference change.
        PreferenceManager.getDefaultSharedPreferences(getActivity())
                .registerOnSharedPreferenceChangeListener(preferenceListener);

        // Set list properties
        getListView().setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                    startUpdatingFileIcons();
                } else
                    stopUpdatingFileIcons();
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem,
                                 int visibleItemCount, int totalItemCount) {
            }
        });
        getListView().requestFocus();
        getListView().requestFocusFromTouch();

        // Init flipper
        mFlipper = (ViewFlipper) view.findViewById(R.id.flipper);
        mClipboardInfo = view.findViewById(R.id.clipboard_info);
        mClipboardContent = (TextView) view.findViewById(R.id.clipboard_content);
        mClipboardAction = (TextView) view.findViewById(R.id.clipboard_action);
        mClipboardAction.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ((FileManagerApplication) getActivity().getApplication()).getCopyHelper().clear();
                updateClipboardInfo();

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
                    ActionbarRefreshHelper.activity_invalidateOptionsMenu(getActivity());
            }
        });

        // Get arguments
        if (savedInstanceState == null) {
            mPath = getArguments().getString(FileManagerIntents.EXTRA_DIR_PATH);
            mFilename = getArguments().getString(
                    FileManagerIntents.EXTRA_FILENAME);
        } else {
            mPath = savedInstanceState.getString(INSTANCE_STATE_PATH);
            mFiles = savedInstanceState
                    .getParcelableArrayList(INSTANCE_STATE_FILES);
        }
        pathCheckAndFix();
        renewScanner();
        mAdapter = new FileHolderListAdapter(mFiles, getActivity());

        setListAdapter(mAdapter);
        mScanner.start();

    }

    private void startUpdatingFileIcons() {
        mAdapter.startProcessingThumbnailLoaderQueue();
    }

    private void stopUpdatingFileIcons() {
        mAdapter.stopProcessingThumbnailLoaderQueue();
    }

    public boolean isLoading() {
        return mFlipper.getDisplayedChild() == 0;
    }

    @Override
    public void onDestroy() {
        mScanner.cancel();
        super.onDestroy();
    }

    /**
     * Reloads {@link #mPath}'s contents.
     */
    public void refresh() {
        if (hasPermissions()) {
            // Cancel and GC previous scanner so that it doesn't load on top of the
            // new list.
            // Race condition seen if a long list is requested, and a short list is
            // requested before the long one loads.
            mScanner.cancel();
            mScanner = null;

            // Indicate loading and start scanning.
            setLoading(true);
            renewScanner().start();
        } else {
            requestPermissions();
        }
    }


    private boolean hasPermissions() {
        return checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED;
    }

    private void requestPermissions() {
        showLoading(true);
        requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_STORAGE_PERMISSION);
    }

    /**
     * Switch to permission request mode.
     */
    private void showPermissionDenied() {
        Toast.makeText(getActivity(), R.string.details_permissions, Toast.LENGTH_SHORT).show();
    }

    /**
     * Make the UI indicate loading.
     */
    private void showLoading(boolean loading) {
    }


    /**
     * Make the UI indicate loading.
     */
    private void setLoading(boolean show) {
        mFlipper.setDisplayedChild(show ? 0 : 1);
        onLoadingChanged(show);
    }

    public void setResourceCallback(IdlingResource.ResourceCallback resourceCallback) {
        this.resourceCallback = resourceCallback;
    }

    protected void selectInList(File selectFile) {
        String filename = selectFile.getName();

        int count = mAdapter.getCount();
        for (int i = 0; i < count; i++) {
            FileHolder it = (FileHolder) mAdapter.getItem(i);
            if (it.getName().equals(filename)) {
                getListView().setSelection(i);
                break;
            }
        }
    }

    /**
     * Recreates the {@link #mScanner} using the previously set arguments and
     * {@link #mPath}.
     *
     * @return {@link #mScanner} for convenience.
     */
    protected DirectoryScanner renewScanner() {
        String filetypeFilter = getArguments().getString(
                FileManagerIntents.EXTRA_FILTER_FILETYPE);
        String mimetypeFilter = getArguments().getString(
                FileManagerIntents.EXTRA_FILTER_MIMETYPE);
        boolean writeableOnly = getArguments().getBoolean(
                FileManagerIntents.EXTRA_WRITEABLE_ONLY);
        boolean directoriesOnly = getArguments().getBoolean(
                FileManagerIntents.EXTRA_DIRECTORIES_ONLY);

        mScanner = new DirectoryScanner(new File(mPath), getActivity(),
                new FileListMessageHandler(),
                MimeTypes.getInstance(),
                filetypeFilter == null ? "" : filetypeFilter,
                mimetypeFilter == null ? "" : mimetypeFilter, writeableOnly,
                directoriesOnly);
        return mScanner;
    }

    private class FileListMessageHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {

            switch (msg.what) {
                case DirectoryScanner.MESSAGE_SHOW_DIRECTORY_CONTENTS:
                    if (getActivity() == null) {
                        return;
                    }

                    DirectoryContents c = (DirectoryContents) msg.obj;
                    mFiles.clear();
                    mFiles.addAll(c.listSdCard);
                    mFiles.addAll(c.listDir);
                    mFiles.addAll(c.listFile);

                    mAdapter.notifyDataSetChanged();


                    if (mPreviousDirectory != null) {
                        selectInList(mPreviousDirectory);
                    } else {
                        // Reset list position.
                        if (!mFiles.isEmpty())
                            getListView().setSelection(0);
                    }
                    setLoading(false);
                    updateClipboardInfo();
                    if (resourceCallback != null) {
                        resourceCallback.onTransitionToIdle();
                    }
                    break;
                case DirectoryScanner.MESSAGE_SET_PROGRESS:
                    // ignore
                    break;
            }
        }
    }

    public void updateClipboardInfo() {
        CopyHelper copyHelper = ((FileManagerApplication) getActivity().getApplication()).getCopyHelper();
        if (copyHelper.canPaste()) {
            mClipboardInfo.setVisibility(View.VISIBLE);
            int count = copyHelper.getItemsCount();
            if (CopyHelper.Operation.COPY.equals(copyHelper.getOperationType())) {
                mClipboardContent.setText(getResources().getQuantityString(R.plurals.clipboard_info_items_to_copy, count, count));
                mClipboardAction.setText(getString(R.string.clipboard_dismiss));
            } else if (CopyHelper.Operation.CUT.equals(copyHelper.getOperationType())) {
                mClipboardContent.setText(getResources().getQuantityString(R.plurals.clipboard_info_items_to_move, count, count));
                mClipboardAction.setText(getString(R.string.clipboard_undo));
            }
        } else {
            mClipboardInfo.setVisibility(View.GONE);
        }
    }

    /**
     * Used to inform subclasses about loading state changing. Can be used to
     * make the ui indicate the loading state of the fragment.
     *
     * @param loading If the list started or stopped loading.
     */
    protected void onLoadingChanged(boolean loading) {
    }

    /**
     * @return The currently displayed directory's absolute path.
     */
    public final String getPath() {
        return mPath;
    }

    /**
     * This will be ignored if path doesn't pass check as valid.
     *
     * @param dir The path to set.
     */
    public final void setPath(File dir) {
        if (dir.exists() && dir.isDirectory()) {
            mPreviousDirectory = mCurrentDirectory;
            mCurrentDirectory = dir;
            mPath = dir.getAbsolutePath();
        }
    }

    private void pathCheckAndFix() {
        File file = new File(mPath);
        // Sanity check that the path (coming from extras_dir_path) is indeed a
        // directory
        if (!file.isDirectory() && file.getParentFile() != null) {
            // remember the filename for picking.
            mFilename = file.getName();
            mPath = file.getParentFile().getAbsolutePath();
        }
    }

    public String getFilename() {
        return mFilename;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_STORAGE_PERMISSION:
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) {
                    refresh();
                } else {
                    showPermissionDenied();
                }
                break;
        }
    }
}
<commit_msg>improve permission handling<commit_after>package org.openintents.filemanager.lists;

import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.test.espresso.IdlingResource;
import android.support.v4.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewFlipper;

import org.openintents.filemanager.FileHolderListAdapter;
import org.openintents.filemanager.FileManagerApplication;
import org.openintents.filemanager.R;
import org.openintents.filemanager.compatibility.ActionbarRefreshHelper;
import org.openintents.filemanager.files.DirectoryContents;
import org.openintents.filemanager.files.DirectoryScanner;
import org.openintents.filemanager.files.FileHolder;
import org.openintents.filemanager.util.CopyHelper;
import org.openintents.filemanager.util.MimeTypes;
import org.openintents.intents.FileManagerIntents;

import java.io.File;
import java.util.ArrayList;

import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.support.v4.content.ContextCompat.checkSelfPermission;


/**
 * A {@link ListFragment} that displays the contents of a directory.
 * <p>
 * Clicks do nothing.
 * </p>
 * <p>
 * Refreshes OnSharedPreferenceChange
 * </p>
 *
 * @author <NAME>
 */
public abstract class FileListFragment extends ListFragment {
    private static final String INSTANCE_STATE_PATH = "path";
    private static final String INSTANCE_STATE_FILES = "files";
    private static final int REQUEST_CODE_STORAGE_PERMISSION = 1;
    File mPreviousDirectory = null;

    // Not an anonymous inner class because of:
    // http://stackoverflow.com/questions/2542938/sharedpreferences-onsharedpreferencechangelistener-not-being-called-consistently
    private OnSharedPreferenceChangeListener preferenceListener = new OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(
                SharedPreferences sharedPreferences, String key) {
            // We only care for list-altering preferences. This could be
            // dangerous though,
            // as later contributors might not see this, and have their settings
            // not work in realtime.
            // Therefore this is commented out, since it's not likely the
            // refresh is THAT heavy.
            // *****************
            // if (PreferenceActivity.PREFS_DISPLAYHIDDENFILES.equals(key)
            // || PreferenceActivity.PREFS_SORTBY.equals(key)
            // || PreferenceActivity.PREFS_ASCENDING.equals(key))

            // Prevent NullPointerException caused from this getting called
            // after we have finish()ed the activity.
            if (getActivity() != null)
                refresh();
        }
    };

    protected FileHolderListAdapter mAdapter;
    protected DirectoryScanner mScanner;
    protected ArrayList<FileHolder> mFiles = new ArrayList<>();
    private String mPath;
    private String mFilename;

    private ViewFlipper mFlipper;
    private File mCurrentDirectory;
    private View mClipboardInfo;
    private TextView mClipboardContent;
    private TextView mClipboardAction;
    private IdlingResource.ResourceCallback resourceCallback;

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putString(INSTANCE_STATE_PATH, mPath);
        outState.putParcelableArrayList(INSTANCE_STATE_FILES, mFiles);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.filelist, null);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {

        // Set auto refresh on preference change.
        PreferenceManager.getDefaultSharedPreferences(getActivity())
                .registerOnSharedPreferenceChangeListener(preferenceListener);

        // Set list properties
        getListView().setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
                    startUpdatingFileIcons();
                } else
                    stopUpdatingFileIcons();
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem,
                                 int visibleItemCount, int totalItemCount) {
            }
        });
        getListView().requestFocus();
        getListView().requestFocusFromTouch();

        // Init flipper
        mFlipper = (ViewFlipper) view.findViewById(R.id.flipper);
        mClipboardInfo = view.findViewById(R.id.clipboard_info);
        mClipboardContent = (TextView) view.findViewById(R.id.clipboard_content);
        mClipboardAction = (TextView) view.findViewById(R.id.clipboard_action);
        mClipboardAction.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ((FileManagerApplication) getActivity().getApplication()).getCopyHelper().clear();
                updateClipboardInfo();

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
                    ActionbarRefreshHelper.activity_invalidateOptionsMenu(getActivity());
            }
        });

        // Get arguments
        if (savedInstanceState == null) {
            mPath = getArguments().getString(FileManagerIntents.EXTRA_DIR_PATH);
            mFilename = getArguments().getString(
                    FileManagerIntents.EXTRA_FILENAME);
        } else {
            mPath = savedInstanceState.getString(INSTANCE_STATE_PATH);
            mFiles = savedInstanceState
                    .getParcelableArrayList(INSTANCE_STATE_FILES);
        }
        pathCheckAndFix();
        renewScanner();
        mAdapter = new FileHolderListAdapter(mFiles, getActivity());

        setListAdapter(mAdapter);
        if (hasPermissions()) {
            mScanner.start();
        } else {
            requestPermissions();
        }
    }

    private void startUpdatingFileIcons() {
        mAdapter.startProcessingThumbnailLoaderQueue();
    }

    private void stopUpdatingFileIcons() {
        mAdapter.stopProcessingThumbnailLoaderQueue();
    }

    public boolean isLoading() {
        return mFlipper.getDisplayedChild() == 0;
    }

    @Override
    public void onDestroy() {
        mScanner.cancel();
        super.onDestroy();
    }

    /**
     * Reloads {@link #mPath}'s contents.
     */
    public void refresh() {
        if (hasPermissions()) {
            // Cancel and GC previous scanner so that it doesn't load on top of the
            // new list.
            // Race condition seen if a long list is requested, and a short list is
            // requested before the long one loads.
            mScanner.cancel();
            mScanner = null;

            // Indicate loading and start scanning.
            setLoading(true);
            renewScanner().start();
        } else {
            requestPermissions();
        }
    }


    private boolean hasPermissions() {
        return checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED;
    }

    private void requestPermissions() {
        setLoading(true);
        requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_STORAGE_PERMISSION);
    }

    /**
     * Switch to permission request mode.
     */
    private void showPermissionDenied() {
        setLoading(false);
        Toast.makeText(getActivity(), R.string.details_permissions, Toast.LENGTH_SHORT).show();
    }

    /**
     * Make the UI indicate loading.
     */
    private void setLoading(boolean show) {
        mFlipper.setDisplayedChild(show ? 0 : 1);
        onLoadingChanged(show);
    }

    public void setResourceCallback(IdlingResource.ResourceCallback resourceCallback) {
        this.resourceCallback = resourceCallback;
    }

    protected void selectInList(File selectFile) {
        String filename = selectFile.getName();

        int count = mAdapter.getCount();
        for (int i = 0; i < count; i++) {
            FileHolder it = (FileHolder) mAdapter.getItem(i);
            if (it.getName().equals(filename)) {
                getListView().setSelection(i);
                break;
            }
        }
    }

    /**
     * Recreates the {@link #mScanner} using the previously set arguments and
     * {@link #mPath}.
     *
     * @return {@link #mScanner} for convenience.
     */
    protected DirectoryScanner renewScanner() {
        String filetypeFilter = getArguments().getString(
                FileManagerIntents.EXTRA_FILTER_FILETYPE);
        String mimetypeFilter = getArguments().getString(
                FileManagerIntents.EXTRA_FILTER_MIMETYPE);
        boolean writeableOnly = getArguments().getBoolean(
                FileManagerIntents.EXTRA_WRITEABLE_ONLY);
        boolean directoriesOnly = getArguments().getBoolean(
                FileManagerIntents.EXTRA_DIRECTORIES_ONLY);

        mScanner = new DirectoryScanner(new File(mPath), getActivity(),
                new FileListMessageHandler(),
                MimeTypes.getInstance(),
                filetypeFilter == null ? "" : filetypeFilter,
                mimetypeFilter == null ? "" : mimetypeFilter, writeableOnly,
                directoriesOnly);
        return mScanner;
    }

    private class FileListMessageHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {

            switch (msg.what) {
                case DirectoryScanner.MESSAGE_SHOW_DIRECTORY_CONTENTS:
                    if (getActivity() == null) {
                        return;
                    }

                    DirectoryContents c = (DirectoryContents) msg.obj;
                    mFiles.clear();
                    mFiles.addAll(c.listSdCard);
                    mFiles.addAll(c.listDir);
                    mFiles.addAll(c.listFile);

                    mAdapter.notifyDataSetChanged();


                    if (mPreviousDirectory != null) {
                        selectInList(mPreviousDirectory);
                    } else {
                        // Reset list position.
                        if (!mFiles.isEmpty())
                            getListView().setSelection(0);
                    }
                    setLoading(false);
                    updateClipboardInfo();
                    if (resourceCallback != null) {
                        resourceCallback.onTransitionToIdle();
                    }
                    break;
                case DirectoryScanner.MESSAGE_SET_PROGRESS:
                    // ignore
                    break;
            }
        }
    }

    public void updateClipboardInfo() {
        CopyHelper copyHelper = ((FileManagerApplication) getActivity().getApplication()).getCopyHelper();
        if (copyHelper.canPaste()) {
            mClipboardInfo.setVisibility(View.VISIBLE);
            int count = copyHelper.getItemsCount();
            if (CopyHelper.Operation.COPY.equals(copyHelper.getOperationType())) {
                mClipboardContent.setText(getResources().getQuantityString(R.plurals.clipboard_info_items_to_copy, count, count));
                mClipboardAction.setText(getString(R.string.clipboard_dismiss));
            } else if (CopyHelper.Operation.CUT.equals(copyHelper.getOperationType())) {
                mClipboardContent.setText(getResources().getQuantityString(R.plurals.clipboard_info_items_to_move, count, count));
                mClipboardAction.setText(getString(R.string.clipboard_undo));
            }
        } else {
            mClipboardInfo.setVisibility(View.GONE);
        }
    }

    /**
     * Used to inform subclasses about loading state changing. Can be used to
     * make the ui indicate the loading state of the fragment.
     *
     * @param loading If the list started or stopped loading.
     */
    protected void onLoadingChanged(boolean loading) {
    }

    /**
     * @return The currently displayed directory's absolute path.
     */
    public final String getPath() {
        return mPath;
    }

    /**
     * This will be ignored if path doesn't pass check as valid.
     *
     * @param dir The path to set.
     */
    public final void setPath(File dir) {
        if (dir.exists() && dir.isDirectory()) {
            mPreviousDirectory = mCurrentDirectory;
            mCurrentDirectory = dir;
            mPath = dir.getAbsolutePath();
        }
    }

    private void pathCheckAndFix() {
        File file = new File(mPath);
        // Sanity check that the path (coming from extras_dir_path) is indeed a
        // directory
        if (!file.isDirectory() && file.getParentFile() != null) {
            // remember the filename for picking.
            mFilename = file.getName();
            mPath = file.getParentFile().getAbsolutePath();
        }
    }

    public String getFilename() {
        return mFilename;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_STORAGE_PERMISSION:
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) {
                    refresh();
                } else {
                    showPermissionDenied();
                }
                break;
        }
    }
}

{'fraction_non_alphanumeric': 0.08787377170429331, 'fraction_numerical': 0.0011974782516817526, 'mean_word_length': 2.280268022181146, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1919818', 'n_tokens_mistral': 7395, 'n_tokens_neox': 7077, 'n_words': 1578}