public final class MediaHelper {

    private static final String EMULATED_STORAGE_SOURCE = System.getenv("EMULATED_STORAGE_SOURCE");
    private static final String EMULATED_STORAGE_TARGET = System.getenv("EMULATED_STORAGE_TARGET");
    private static final String EXTERNAL_STORAGE = System.getenv("EXTERNAL_STORAGE");

    private static final String INTERNAL_VOLUME = "internal";
    private static final String EXTERNAL_VOLUME = "external";

    /**
     * URIs that are relevant for determining album art;
     * useful for content observer registration
     */
    public static final Uri[] RELEVANT_URIS = new Uri[] {
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI
    };

    /**
     * Method that returns an array with all the unique albums paths and ids.
     *
     * @param cr The ContentResolver
     * @return The albums map
     */
    public static Map<String, Long> getAllAlbums(ContentResolver cr) {
        final Map<String, Long> albums = new HashMap<>();
        final String[] projection =
                {
                        "distinct " + MediaStore.Audio.Media.ALBUM_ID,
                        "substr(" + MediaStore.Audio.Media.DATA + ", 0, length(" +
                                MediaStore.Audio.Media.DATA + ") - length(" +
                                MediaStore.Audio.Media.DISPLAY_NAME + "))"
                };
        final String where = MediaStore.Audio.Media.IS_MUSIC + " = ?";
        Cursor c = cr.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                projection, where, new String[]{"1"}, null);
        if (c != null) {
            try {
                for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
                    long albumId = c.getLong(0);
                    String albumPath = c.getString(1);
                    albums.put(albumPath, albumId);
                }
            } finally {
                IOUtils.closeQuietly(c);
            }
        }
        return albums;
    }

    /**
     * Method that returns the album thumbnail path by its identifier.
     *
     * @param cr The ContentResolver
     * @param albumId The album identifier to search
     * @return String The album thumbnail path
     */
    public static String getAlbumThumbnailPath(@NonNull final ContentResolver cr,
                                               final long albumId) {
        final String[] projection = {MediaStore.Audio.Albums.ALBUM_ART};
        final String where = BaseColumns._ID + "=?";
        Cursor c = cr.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
                projection, where, new String[]{String.valueOf(albumId)}, null);
        try {
            if (c != null && c.moveToFirst()) {
                return c.getString(0);
            }
        } finally {
            IOUtils.closeQuietly(c);
        }
        return null;
    }

    /**
     * Method that converts a file reference to a content uri reference
     *
     * @param cr A content resolver
     * @param file The file reference
     * @return Uri The content uri or null if file not exists in the media database
     */
    public static Uri fileToContentUri(ContentResolver cr, File file) {
        // Normalize the path to ensure media search
        final String normalizedPath = normalizeMediaPath(file.getAbsolutePath());

        // Check in external and internal storages
        Uri uri = fileToContentUri(cr, normalizedPath, EXTERNAL_VOLUME);
        if (uri != null) {
            return uri;
        }
        uri = fileToContentUri(cr, normalizedPath, INTERNAL_VOLUME);
        if (uri != null) {
            return uri;
        }
        return null;
    }

    /**
     * Method that converts a file reference to a content uri reference
     *
     * @param cr A content resolver
     * @param path The path to search
     * @param volume The volume
     * @return Uri The content uri or null if file not exists in the media database
     */
    private static Uri fileToContentUri(ContentResolver cr, String path, String volume) {
        final String[] projection = {BaseColumns._ID, MediaStore.Files.FileColumns.MEDIA_TYPE};
        final String where = MediaColumns.DATA + " = ?";
        Uri baseUri = MediaStore.Files.getContentUri(volume);
        Cursor c = cr.query(baseUri, projection, where, new String[]{path}, null);
        try {
            if (c != null && c.moveToNext()) {
                int type = c.getInt(c.getColumnIndexOrThrow(
                        MediaStore.Files.FileColumns.MEDIA_TYPE));
                if (type != 0) {
                    // Do not force to use content uri for no media files
                    long id = c.getLong(c.getColumnIndexOrThrow(BaseColumns._ID));
                    return Uri.withAppendedPath(baseUri, String.valueOf(id));
                }
            }
        } finally {
            IOUtils.closeQuietly(c);
        }
        return null;
    }

    /**
     * Method that converts a content uri to a file system path
     *
     * @param cr The content resolver
     * @param uri The content uri
     * @return File The file reference
     */
    public static File contentUriToFile(ContentResolver cr, Uri uri) {
        // Sanity checks
        if (uri == null || uri.getScheme() == null || uri.getScheme().compareTo("content") != 0) {
            return null;
        }

        // Retrieve the request id
        long id;
        try {
            id = Long.parseLong(new File(uri.getPath()).getName());
        } catch (NumberFormatException nfex) {
            return null;
        }

        // Check in external and internal storages
        File file = mediaIdToFile(cr, id, EXTERNAL_VOLUME);
        if (file != null) {
            return file;
        }
        file = mediaIdToFile(cr, id, INTERNAL_VOLUME);
        if (file != null) {
            return file;
        }
        return null;
    }

    /**
     * Method that converts a content uri to a file system path
     *
     * @param cr The content resolver
     * @param id The media database id
     * @param volume The volume
     * @return File The file reference
     */
    private static File mediaIdToFile(ContentResolver cr, long id, String volume) {
        final String[] projection = {MediaColumns.DATA};
        final String where = MediaColumns._ID + " = ?";
        Uri baseUri = MediaStore.Files.getContentUri(volume);
        Cursor c = cr.query(baseUri, projection, where, new String[]{String.valueOf(id)}, null);
        try {
            if (c != null && c.moveToNext()) {
                return new File(c.getString(c.getColumnIndexOrThrow(MediaColumns.DATA)));
            }
        } finally {
            if (c != null) {
                c.close();
            }
        }
        return null;
    }

    /**
     * Method that converts a not standard media mount path to a standard media path
     *
     * @param path The path to normalize
     * @return String The normalized media path
     */
    public static String normalizeMediaPath(String path) {
        // Retrieve all the paths and check that we have this environment vars
        if (TextUtils.isEmpty(EMULATED_STORAGE_SOURCE) ||
                TextUtils.isEmpty(EMULATED_STORAGE_TARGET) ||
                TextUtils.isEmpty(EXTERNAL_STORAGE)) {
            return path;
        }

        // We need to convert EMULATED_STORAGE_SOURCE -> EMULATED_STORAGE_TARGET
        if (path.startsWith(EMULATED_STORAGE_SOURCE)) {
            path = path.replace(EMULATED_STORAGE_SOURCE, EMULATED_STORAGE_TARGET);
        }
        // We need to convert EXTERNAL_STORAGE -> EMULATED_STORAGE_TARGET / userId
        if (path.startsWith(EXTERNAL_STORAGE)) {
            final String userId = String.valueOf(myUserId());
            final String target = new File(EMULATED_STORAGE_TARGET, userId).getAbsolutePath();
            path = path.replace(EXTERNAL_STORAGE, target);
        }
        return path;
    }

    private static int myUserId() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            return UserHandleJellyBeanMR1.myUserId();
        }
        return 0;
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    private static final class UserHandleJellyBeanMR1 {

        @SuppressLint("NewApi")
        static int myUserId() {
            try {
                final Method myUserIdMethod = android.os.UserHandle.class.getMethod("myUserId");
                myUserIdMethod.setAccessible(true);
                return (Integer) myUserIdMethod.invoke(null);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
            return 0;
        }
    }
} public abstract class BrowserBaseAdapter implements ListAdapter,
        MultiListenerFileObserver.OnEventListener {

    /**
     * Events to be monitor for every File in this Adapter
     */
    private static final int OBSERVER_EVENTS =
            FileObserver.CREATE |
            FileObserver.DELETE_SELF |
            FileObserver.ATTRIB |
            FileObserver.MOVED_TO;

    /**
     * Cache that holds file icons
     */
    private static DrawableLruCache<Integer> sDrawableLruCache;

    /**
     * Cache that holds file icons
     */
    private static DrawableLruCache<String> sMimeTypeIconCache;

    @NonNull
    private final Handler mHandler;

    /**
     * Application's {@link android.content.res.Resources}
     */
    @NonNull
    private final Resources mResources;

    /**
     * Current {@link android.content.res.Resources.Theme}
     */
    @NonNull
    private final Resources.Theme mTheme;

    @NonNull
    private final DataSetObservable mDataSetObservable = new DataSetObservable();

    @NonNull
    private final FileObserverCache mObserverCache = FileObserverCache.getInstance();

    /**
     * Adapter's content
     */
    @NonNull
    private final List<GenericFile> mContent = new ArrayList<>();

    /**
     * Observers for Files used in this Adapter
     */
    @NonNull
    private final List<MultiListenerFileObserver> mFileObservers = new ArrayList<>();

    /**
     * Executor for loading file previews
     */
    private ExecutorService mExecutor;

    /**
     * Current FileSortType
     */
    private FileSortType mComparator = FileSortType.NAME_ASC;

    /**
     * Current LayoutInflater
     */
    protected final LayoutInflater mLayoutInflater;

    private final PreviewHolder mPreviewHolder;

    /**
     * Settings instance
     */
    @NonNull
    protected final Settings mSettings;

    protected BrowserBaseAdapter(@NonNull final Activity context) {
        if (sDrawableLruCache == null) {
            sDrawableLruCache = new DrawableLruCache<>();
        }
        if (sMimeTypeIconCache == null) {
            sMimeTypeIconCache = new DrawableLruCache<>();
        }
        mSettings = Settings.getInstance(context);
        mPreviewHolder = PreviewHolder.getInstance(context);
        mTheme = context.getTheme();
        mResources = context.getResources();
        mHandler = new FileObserverEventHandler(this);
        mLayoutInflater = context.getLayoutInflater();
    }

    public void dropCaches() {
        sDrawableLruCache.evictAll();
        sMimeTypeIconCache.evictAll();
    }

    /**
     * Sets and applies new data
     *
     * @param data Data to apply
     */
    public void updateData(GenericFile[] data) {
        if (mExecutor != null) {
            mExecutor.shutdownNow();
        }
        mExecutor = Executors.newSingleThreadExecutor();
        mContent.clear();
        releaseObservers();
        if (data != null) {
            Arrays.sort(data, mComparator.getComparator());
            for (final GenericFile file : data) {
                mContent.add(file);
                final MultiListenerFileObserver observer = mObserverCache
                        .getOrCreate(file, OBSERVER_EVENTS);
                observer.addOnEventListener(this);
                observer.startWatching();
                mFileObservers.add(observer);
            }
        }
        this.notifyDataSetChanged();
    }

    /**
     * Removes references for all {@link android.os.FileObserver}s
     */
    public final void releaseObservers() {
        for (final MultiListenerFileObserver observer : mFileObservers) {
            observer.removeOnEventListener(this);
            observer.stopWatching();
        }
        mFileObservers.clear();
    }

    /**
     * Inserts new items to this Adapter's data to position determined by current FileSortType
     *
     * @param files Files to insert
     */
    public final void addFiles(final GenericFile... files) {
        for (final GenericFile file : files) {
            mContent.add(file);
            mFileObservers.add(mObserverCache.getOrCreate(file, OBSERVER_EVENTS));
        }
        Collections.sort(mContent, mComparator.getComparator());
        notifyDataSetChanged();
    }

    /**
     * Sets and applies {@link com.docd.purefm.utils.FileSortType}
     *
     * @param comp FileSortType to apply
     */
    public void setCompareType(final FileSortType comp) {
        mComparator = comp;
        Collections.sort(mContent, comp.getComparator());
        notifyDataSetChanged();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getCount() {
        return this.mContent.size();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public GenericFile getItem(final int pos) {
        return this.mContent.get(pos);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long getItemId(final int pos) {
        return 0L;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getItemViewType(int pos) {
        return 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getViewTypeCount() {
        return 1;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasStableIds() {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEmpty() {
        return this.mContent.isEmpty();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void registerDataSetObserver(final DataSetObserver arg0) {
        this.mDataSetObservable.registerObserver(arg0);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void unregisterDataSetObserver(final DataSetObserver arg0) {
        this.mDataSetObservable.unregisterObserver(arg0);
    }

    /**
     * Notifies the attached observers that the underlying data has been changed
     * and any View reflecting the data set should refresh itself.
     */
    protected synchronized final void notifyDataSetChanged() {
        this.mDataSetObservable.notifyChanged();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean areAllItemsEnabled() {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEnabled(int arg0) {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onEvent(final int event, final String path) {
        final Message message = mHandler.obtainMessage(
                FileObserverEventHandler.MESSAGE_OBSERVER_EVENT);
        message.arg1 = event;
        message.obj = FileFactory.newFile(mSettings, path);
        mHandler.sendMessage(message);
    }

    /**
     * {@link android.os.FileObserver} event that should be ran only on UI thread
     *
     * @param event The type of event which happened
     * @param file The modified file, relative to the main monitored file or directory,
     *             of the file or directory which triggered the event
     */
    void onEventUIThread(final int event,
                                      @NonNull final GenericFile file) {
        switch (event & FileObserver.ALL_EVENTS) {
            case FileObserver.CREATE:
                //Do nothing. The event is handled in Browser
                break;

            case FileObserver.DELETE:
            case FileObserver.DELETE_SELF:
            case FileObserver.MOVED_FROM:
                onFileDeleted(file);
                break;

            default:
                onFileModified(file);
                break;
        }
    }

    /**
     * Should be called when the file at path was modified
     *
     * @param modified The modified file
     */
    private void onFileModified(@NonNull final GenericFile modified) {
        final GenericFile affectedFile = getFileByPath(modified.getAbsolutePath());
        if (affectedFile != null) {
            final int index = mContent.indexOf(affectedFile);
            if (index != -1) {
                mContent.set(index, modified);
            } else {
                mContent.add(modified);
                Collections.sort(mContent, mComparator.getComparator());
            }
        }
    }

    private void onFileDeleted(@NonNull final GenericFile deleted) {
        final GenericFile affectedFile = getFileByPath(deleted.getAbsolutePath());
        if (affectedFile != null) {
            mContent.remove(affectedFile);
            removeObserverForPath(PFMFileUtils.fullPath(affectedFile));
        }
    }

    @Nullable
    private GenericFile getFileByPath(@NonNull final String path) {
        for (final GenericFile file : mContent) {
            if (file.getAbsolutePath().equals(path)) {
                return file;
            }
            try {
                if (file.getCanonicalPath().equals(path)) {
                    return file;
                }
            } catch (IOException ignored) {
                //ignored
            }
        }
        return null;
    }

    /**
     * Removes {@link android.os.FileObserver} that monitors the path from cache
     *
     * @param path Path to remove FileObserver for
     */
    private void removeObserverForPath(final String path) {
        final int observersSize = mFileObservers.size();
        for (int i = 0; i < observersSize; i++) {
            final MultiListenerFileObserver observer = mFileObservers.get(i);
            if (observer.getPath().equals(path)) {
                observer.stopWatching();
                mFileObservers.remove(i);
                break;
            }
        }
    }

    /**
     * Resolves icon that should be used for the File
     *
     * @param file File to set icon for
     * @param icon View to set icon
     */
    protected final void setIcon(final GenericFile file, final ImageView icon, boolean large) {
        if (file.isDirectory()) {
            icon.setImageDrawable(getDrawableForRes(mResources, large ?
                    R.drawable.ic_fso_folder_large : R.drawable.ic_fso_folder));
        } else {
            final String fileExt = FilenameUtils.getExtension(file.getName());
            Drawable mimeIcon = sMimeTypeIconCache.get(fileExt);
            if (mimeIcon == null) {
                final int mimeIconId = MimeTypes.getIconForExt(fileExt);
                if (mimeIconId != 0) {
                    mimeIcon = mResources.getDrawable(mimeIconId);
                    sMimeTypeIconCache.put(fileExt, mimeIcon);
                }
            }
            if (mimeIcon != null) {
                icon.setImageDrawable(mimeIcon);
            } else {
                final Permissions p = file.getPermissions();
                if (!file.isSymlink() && (p.gx || p.ux || p.ox)) {
                    final int executableIcon = R.drawable.ic_fso_type_executable;
                    Drawable iconDrawable = sDrawableLruCache.get(executableIcon);
                    if (iconDrawable == null) {
                        iconDrawable = mResources.getDrawable(executableIcon);
                        sDrawableLruCache.put(executableIcon, iconDrawable);
                    }
                    icon.setImageDrawable(iconDrawable);
                } else {
                    icon.setImageDrawable(getDrawableForRes(mResources, R.drawable.ic_fso_default));
                }
            }
        }
    }

    /**
     * Applies overlay for File, if should be applied. Removes overlay if not.
     *
     * @param f File to apply overlay for
     * @param overlay View to apply overlay to
     */
    protected final void applyOverlay(GenericFile f, OverlayImageView overlay) {
        if (f.isSymlink()) {
            overlay.setOverlay(getDrawableForRes(mTheme, R.attr.ic_fso_symlink));
        } else {
            overlay.setOverlay(null);
        }
    }

    /**
     * Loads drawable from resources cache. If not found in cache, loads the
     * {@link android.graphics.drawable.Drawable} from {@link android.content.res.Resources.Theme}
     *
     * @param theme Theme to load drawable for
     * @param attrId attribute id of resource to load
     * @return Drawable for Theme
     */
    @NonNull
    private static Drawable getDrawableForRes(final Resources.Theme theme, final int attrId) {
        Drawable drawable = sDrawableLruCache.get(attrId);
        if (drawable == null) {
            drawable = ThemeUtils.getDrawableNonNull(theme, attrId);
            sDrawableLruCache.put(attrId, drawable);
        }
        return drawable;
    }

    /**
     * Loads drawable from resources cache. If not found in cache, loads the
     * {@link android.graphics.drawable.Drawable} from {@link android.content.res.Resources}
     *
     * @param res Resources to load drawable from
     * @param resId Id of resource to load
     * @return Drawable from resources
     */
    @NonNull
    private static Drawable getDrawableForRes(final Resources res, final int resId) {
        Drawable drawable = sDrawableLruCache.get(resId);
        if (drawable == null) {
            drawable = res.getDrawable(resId);
            sDrawableLruCache.put(resId, drawable);
        }
        return drawable;
    }

    /**
     * Loads preview from cache. If the preview in cache is not found it starts new
     * {@link com.docd.purefm.adapters.BrowserBaseAdapter.Job} for loading preview from file
     *
     * @param file File to load preview for
     * @param logo View to set loaded preview to
     */
    protected final void loadPreview(@NonNull final GenericFile file,
                                     @NonNull final OverlayImageView logo) {
        final Bitmap result = mPreviewHolder.getCached(file.toFile());
        if (result != null) {
            logo.setImageBitmap(result);
        } else {
            try {
                mExecutor.submit(new Job(mHandler, mPreviewHolder, file, logo));
            } catch (Exception e) {
                Log.w("BrowserBaseAdapter", "Error submitting Job:" + e);
            }
        }
    }

    private static final class FileObserverEventHandler extends Handler {

        static final int MESSAGE_OBSERVER_EVENT = 666;

        @NonNull
        private final WeakReference<BrowserBaseAdapter> mAdapterReference;

        FileObserverEventHandler(@NonNull final BrowserBaseAdapter adapter) {
            super(Looper.getMainLooper());
            this.mAdapterReference = new WeakReference<>(adapter);
        }

        @Override
        public void handleMessage(final Message msg) {
            if (msg.what == MESSAGE_OBSERVER_EVENT) {
                final BrowserBaseAdapter adapter = mAdapterReference.get();
                if (adapter != null) {
                    adapter.onEventUIThread(msg.arg1, (GenericFile) msg.obj);
                    if (!hasMessages(MESSAGE_OBSERVER_EVENT)) {
                        adapter.notifyDataSetChanged();
                    }
                }
            } else {
                super.handleMessage(msg);
            }
        }
    }

    /**
     * Executor job for loading preview from file
     */
    private static final class Job
            implements Runnable
    {

        @NonNull
        private final Handler mHandler;

        @NonNull
        private final PreviewHolder mPreviewHolder;

        @NonNull
        private final OverlayImageView mImageView;

        @NonNull
        private final GenericFile mFile;

        Job(@NonNull final Handler handler,
            @NonNull final PreviewHolder previewHolder,
            @NonNull final GenericFile file,
            @NonNull final OverlayImageView imageView)
        {
            this.mHandler = handler;
            this.mPreviewHolder = previewHolder;
            this.mFile = file;
            this.mImageView = imageView;
            imageView.setTag(file);
        }

        @Override
        public void run()
        {
            final Thread t = Thread.currentThread();
            t.setPriority(Thread.NORM_PRIORITY - 1);
            
            final Bitmap result = mPreviewHolder.loadPreview(this.mFile.toFile());
            if (result != null && this.mImageView.getTag().equals(this.mFile)) {
                this.mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        mImageView.setImageBitmap(result);
                        mImageView.setOverlay(null);
                    }
                });
            }
        }
    }

} public final class BrowserGridAdapter extends BrowserBaseAdapter {

    public BrowserGridAdapter(@NonNull final Activity context) {
        super(context);
    }

    @Override
    public View getView(int pos, View v, ViewGroup arg2) {
        if (pos >= this.getCount()) {
            //workaround for indexOutOfBoundsException when deleting from adapter
            // and notifyDataSetChanged is not yet called
            return v;
        }

        Holder h;
        
        if (v == null) {
            v = this.mLayoutInflater.inflate(R.layout.grid_item_file, arg2, false);
            if (v == null) {
                throw new RuntimeException("Inflated view is null");
            }
            h = new Holder();
            h.mIcon = (OverlayImageView) v.findViewById(android.R.id.icon);
            h.mTitle = (TextView) v.findViewById(android.R.id.title);
            v.setTag(h);
        } else {
            h = (Holder) v.getTag();
        }
        
        final GenericFile f = this.getItem(pos);
        this.setIcon(f, h.mIcon, true);
        this.applyOverlay(f, h.mIcon);
        
        if (mSettings.listShowPreviewsEnabled()) {
            loadPreview(f, h.mIcon);
        }
        h.mTitle.setText(f.getName());
        
        return v;
    }
    
    private static final class Holder {
        
        Holder() {}
        
        OverlayImageView mIcon;
        TextView mTitle;
    }

} public final class BrowserListAdapter extends BrowserBaseAdapter {

    @NonNull
    private final Typeface mTypefaceMonospace;

    @NonNull
    private final LongSparseArray<String> mHumanReadableLastModified = new LongSparseArray<>();

    @NonNull
    private final LongSparseArray<String> mHumanReadableLength = new LongSparseArray<>();
    
    public BrowserListAdapter(@NonNull final Activity context) {
        super(context);
        mTypefaceMonospace = Typeface.createFromAsset(context.getAssets(), "DroidSansMono.ttf");
    }

    @Override
    public View getView(int pos, View v, ViewGroup arg2) {
        if (pos >= this.getCount()) {
            //workaround for indexOutOfBoundsException when deleting from adapter
            // and notifyDataSetChanged is not yet called
            return v;
        }
        
        Holder h;
        
        if (v == null) {
            v = this.mLayoutInflater
                    .inflate(R.layout.list_item_file, arg2, false);
            if (v == null) {
                throw new RuntimeException("Inflated View is null");
            }
            h = new Holder();
            h.mIcon = (OverlayImageView) v.findViewById(android.R.id.icon);
            h.mTitle = (TextView) v.findViewById(android.R.id.title);
            h.mDate = (TextView) v.findViewById(android.R.id.text1);
            h.mPerm = (TextView) v.findViewById(android.R.id.text2);
            h.mSize = (TextView) v.findViewById(R.id.size);

            h.mDate.setTypeface(this.mTypefaceMonospace);
            h.mPerm.setTypeface(this.mTypefaceMonospace);
            h.mSize.setTypeface(this.mTypefaceMonospace);
            v.setTag(h);
        } else {
            h = (Holder) v.getTag();
        }
        
        final GenericFile f = this.getItem(pos);
        this.setIcon(f, h.mIcon, false);
        this.applyOverlay(f, h.mIcon);

        if (mSettings.listShowPreviewsEnabled()) {
            loadPreview(f, h.mIcon);
        }
        h.mTitle.setText(f.getName());

        if (mSettings.listShowModifiedDateEnabled()) {
            final long lastModified = f.lastModified();
            String humanReadableLastModified = mHumanReadableLastModified.get(lastModified);
            if (humanReadableLastModified == null) {
                humanReadableLastModified = PFMTextUtils.humanReadableDate(
                        lastModified,
                        f instanceof CommandLineFile);
                mHumanReadableLastModified.put(lastModified, humanReadableLastModified);
            }
            h.mDate.setText(humanReadableLastModified);
        } else {
            h.mDate.setText(null);
        }

        if (mSettings.listShowFileSizeEnabled() && !f.isDirectory()) {
            final long fileSize = f.length();
            String humanReadableFileSize = mHumanReadableLength.get(fileSize);
            if (humanReadableFileSize == null) {
                humanReadableFileSize = PFMFileUtils.byteCountToDisplaySize(BigInteger.valueOf(fileSize));
                mHumanReadableLength.put(fileSize, humanReadableFileSize);
            }
            h.mSize.setText(humanReadableFileSize);
        } else {
            h.mSize.setText(null);
        }

        if (mSettings.listShowPermissionsEnabled()) {
            h.mPerm.setText(f.getPermissions().toString());
        } else {
            h.mPerm.setText(null);
        }

        return v;
    }
    
    private static final class Holder {
        
        Holder() {}
        
        OverlayImageView mIcon;
        TextView mTitle;
        TextView mSize;
        TextView mDate;
        TextView mPerm;
        
    }

} public final class Browser implements MultiListenerFileObserver.OnEventListener {

    private static final int OBSERVER_EVENTS = FileObserver.CREATE |
            FileObserver.DELETE_SELF |
            FileObserver.MOVED_TO |
            FileObserver.MOVE_SELF;

    private static Handler sHandler;
    private final Context mContext;
    private final Settings mSettings;

    public interface OnNavigateListener {
        void onNavigate(GenericFile path);
        void onNavigationCompleted(GenericFile path);
    }

    private final ArrayDeque<String> mHistory;

    private final FileObserverCache mObserverCache;
    private MultiListenerFileObserver mCurrentPathObserver;
    private GenericFile mCurrentPath;
    private GenericFile mPreviousPath;
    private OnNavigateListener mNavigateListener;

    private Runnable mLastRunnable;
    private boolean mHistoryEnabled;

    private final ResolveInitialPathTask mInitialPathTask;

    public Browser(@NonNull final AbstractBrowserActivity activity, final boolean historyEnabled) {
        if (sHandler == null) {
            sHandler = new Handler(activity.getMainLooper());
        }
        mContext = activity;
        mSettings = Settings.getInstance(activity);
        mHistoryEnabled = historyEnabled;
        mObserverCache = FileObserverCache.getInstance();
        mHistory = new ArrayDeque<>(historyEnabled ? 15 : 0);

        final String home = Settings.getInstance(activity).getHomeDirectory();
        mInitialPathTask = new ResolveInitialPathTask(this, mSettings, home);
        mInitialPathTask.execute();
    }

//    void setInitialPath(final GenericFile currentPath) {
//        mCurrentPath = currentPath;
//        mCurrentPathObserver = mObserverCache.getOrCreate(
//                currentPath, OBSERVER_EVENTS);
//        mCurrentPathObserver.addOnEventListener(this);
//        mCurrentPathObserver.startWatching();
//    }

    private void cancelInitialPathLoading() {
        if (mInitialPathTask.getStatus() == AsyncTask.Status.RUNNING) {
            mInitialPathTask.cancel(true);
        }
    }

    public void setHistoryEnabled(final boolean enabled) {
        if (mHistoryEnabled != enabled) {
            mHistoryEnabled = enabled;
            mHistory.clear();
        }
    }

    public Parcelable saveInstanceState() {
        return new SavedState(mHistory,
                mCurrentPath != null ? mCurrentPath.getAbsolutePath() : null,
                mPreviousPath != null ? mPreviousPath.getAbsolutePath() : null);
    }

    public void restoreState(@Nullable final Parcelable state) {
        final SavedState savedState = (SavedState) state;
        if (savedState != null) {
            mHistory.clear();
            mHistory.addAll(savedState.mHistory);

            if (savedState.mCurrentPath != null) {
                final GenericFile savedStateCurrentFile = FileFactory.newFile(
                        mSettings, savedState.mCurrentPath);
                if (savedStateCurrentFile.exists() &&
                        savedStateCurrentFile.isDirectory()) {
                    cancelInitialPathLoading();
                    mCurrentPath = savedStateCurrentFile;
                }
            }

            if (savedState.mPreviousPath != null) {
                final GenericFile savedStatePreviousFile = FileFactory.newFile(
                        mSettings, savedState.mPreviousPath);
                if (savedStatePreviousFile.exists() &&
                        savedStatePreviousFile.isDirectory()) {
                    mPreviousPath = savedStatePreviousFile;
                }
            }
            invalidate();
        }
    }

    public void setOnNavigateListener(OnNavigateListener l) {
        this.mNavigateListener = l;
    }

    @Nullable
    public GenericFile getCurrentPath() {
        return this.mCurrentPath;
    }
    
    public void onScanFinished(GenericFile requested) {
        mCurrentPath = requested;
        if (mNavigateListener != null) {
            mNavigateListener.onNavigationCompleted(requested);
        }
        if (mCurrentPathObserver != null) {
            mCurrentPathObserver.stopWatching();
            mCurrentPathObserver.removeOnEventListener(this);
        }
        if (requested.exists() && requested.isDirectory()) {
            mCurrentPathObserver = mObserverCache.getOrCreate(requested, OBSERVER_EVENTS);
            mCurrentPathObserver.addOnEventListener(this);
            mCurrentPathObserver.startWatching();
        } else {
            mHistory.remove(requested.getAbsolutePath());
            final GenericFile parent = resolveExistingParent(requested);
            navigate(parent, true);
        }
    }
    
    public void onScanCancelled(final boolean navigateToPrevious) {
        if (navigateToPrevious && mPreviousPath != null) {
            mCurrentPath = this.mPreviousPath;
        }
    }

    @NonNull
    private GenericFile resolveExistingParent(@NonNull final GenericFile file) {
        GenericFile parent = file.getParentFile();
        if (parent == null) {
            return file;
        }
        while (!parent.exists() || !parent.isDirectory()) {
            mHistory.remove(parent.getAbsolutePath());
            final GenericFile previousParent = parent;
            parent = parent.getParentFile();
            if (parent == null) {
                return previousParent;
            }
        }
        return parent;
    }

    public void navigate(@NonNull final GenericFile target, final boolean addToHistory) {
        cancelInitialPathLoading();
        if (target.exists()) {
            if (target.isDirectory()) {
                if (!target.equals(mCurrentPath)) {
                    mPreviousPath = mCurrentPath;
                    mCurrentPath = target;
                    if (addToHistory && mHistoryEnabled && mPreviousPath != null) {
                        mHistory.push(mPreviousPath.getAbsolutePath());
                    }
                    invalidate();
                }
                return;
            } else {
                Log.w("Browser", "The target is not a directory");
                Toast.makeText(mContext, R.string.target_is_not_a_directory,
                        Toast.LENGTH_SHORT).show();
            }
        } else {
            Log.w("Browser",
                    "Trying to navigate to non-existing directory. Searching for existing parent");
            Toast.makeText(mContext, mContext.getString(R.string.directory_not_exists,
                    PFMFileUtils.fullPath(target)), Toast.LENGTH_SHORT).show();
        }
        final GenericFile parent = resolveExistingParent(target);
        if (!parent.equals(target)) {
            navigate(parent, addToHistory);
        }
    }
    
    public boolean back() {
        if (mCurrentPath == null) {
            return false;
        }
        cancelInitialPathLoading();
        if (!this.mHistory.isEmpty()) {
            GenericFile f = FileFactory.newFile(mSettings, mHistory.pop());
            while (!this.mHistory.isEmpty() && !f.exists()) {
                f = FileFactory.newFile(mSettings, this.mHistory.pop());
            }
            if (f.exists() && f.isDirectory()) {
                this.navigate(f, false);
                return true;
            }
        }
        return false;
    }
    
    public void up() {
        if (mCurrentPath == null) {
            return;
        }
        cancelInitialPathLoading();
        if (mCurrentPath.toFile().equals(
                com.docd.purefm.Environment.sRootDirectory)) {
            return;
        }
        final GenericFile parent = resolveExistingParent(mCurrentPath);
        this.mHistory.push(parent.getAbsolutePath());
        this.navigate(parent, true);
    }
    
    public void invalidate() {
        if (mNavigateListener != null && mCurrentPath != null) {
            mNavigateListener.onNavigate(mCurrentPath);
        }
    }

    public boolean isRoot() {
        return this.mCurrentPath.toFile().equals(com.docd.purefm.Environment.sRootDirectory);
    }

    @Override
    public void onEvent(int event, String pathString) {
        switch (event & FileObserver.ALL_EVENTS) {
            case FileObserver.MOVE_SELF:
            case FileObserver.DELETE_SELF:
                sHandler.removeCallbacks(mLastRunnable);
                final GenericFile parent = resolveExistingParent(mCurrentPath);
                sHandler.post(mLastRunnable = new NavigateRunnable(
                        Browser.this, parent, true));
                break;

            case FileObserver.CREATE:
            case FileObserver.MOVED_TO:
                sHandler.removeCallbacks(mLastRunnable);
                sHandler.post(mLastRunnable = new InvalidateRunnable(this));
                break;
        }
    }

    private static final class SavedState implements Parcelable {
        final ArrayDeque<String> mHistory;
        final String mCurrentPath;
        final String mPreviousPath;

        SavedState(@NonNull final ArrayDeque<String> history,
                   @Nullable final String currentPath,
                   @Nullable final String previousPath) {
            this.mHistory = history;
            this.mCurrentPath = currentPath;
            this.mPreviousPath = previousPath;
        }

        @SuppressWarnings("unchecked")
        SavedState(final Parcel source) {
            this.mHistory = (ArrayDeque<String>) source.readSerializable();
            this.mCurrentPath = source.readString();
            this.mPreviousPath = source.readString();
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeSerializable(this.mHistory);
            dest.writeString(this.mCurrentPath);
            dest.writeString(this.mPreviousPath);
        }

        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
            @NonNull
            @Override
            public SavedState createFromParcel(Parcel source) {
                return new SavedState(source);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

    private static final class ResolveInitialPathTask extends AsyncTask<Void, Void, GenericFile> {

        private final WeakReference<Browser> mBrowserReference;
        private final String mHomeDirectory;
        private final Settings mSettings;

        private ResolveInitialPathTask(@NonNull final Browser browser,
                                       @NonNull final Settings settings,
                                       @Nullable final String homeDirectory) {
            this.mBrowserReference = new WeakReference<>(browser);
            this.mSettings = settings;
            this.mHomeDirectory = homeDirectory;
        }

        @Override
        protected GenericFile doInBackground(Void... params) {
            GenericFile initialFile = null;
            if (mHomeDirectory != null) {
                final GenericFile currentFile = FileFactory.newFile(mSettings, mHomeDirectory);
                if (currentFile.exists() && currentFile.isDirectory()) {
                    initialFile = currentFile;
                }
            }
            if (initialFile == null) {
                final String state = Environment.getExternalStorageState();
                if (state.equals(Environment.MEDIA_MOUNTED) ||
                        state.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) {
                    initialFile = FileFactory.newFile(mSettings,
                            Environment.getExternalStorageDirectory());
                }
            }
            if (initialFile == null) {
                initialFile = FileFactory.newFile(mSettings,
                        com.docd.purefm.Environment.sRootDirectory.getAbsolutePath());
            }
            return initialFile;
        }

        @Override
        protected void onPostExecute(final GenericFile genericFile) {
            final Browser browser = mBrowserReference.get();
            if (browser != null) {
                browser.navigate(genericFile, false);
            }
        }
    }

    private static final class InvalidateRunnable implements Runnable {
        private final WeakReference<Browser> mBrowserReference;
        InvalidateRunnable(final Browser browser) {
            this.mBrowserReference = new WeakReference<>(browser);
        }

        @Override
        public void run() {
            final Browser browser = this.mBrowserReference.get();
            if (browser != null) {
                browser.invalidate();
            }
        }
    }

    private static final class NavigateRunnable implements Runnable {
        private final WeakReference<Browser> mBrowserReference;
        private final GenericFile mTarget;
        private final boolean mAddToHistory;

        NavigateRunnable(final Browser browser,
                         final GenericFile target,
                         final boolean addToHistory) {
            this.mBrowserReference = new WeakReference<>(browser);
            this.mTarget = target;
            this.mAddToHistory = addToHistory;
        }

        @Override
        public void run() {
            final Browser browser = this.mBrowserReference.get();
            if (browser != null) {
                browser.navigate(this.mTarget, this.mAddToHistory);
            }
        }
    }
} public interface OnNavigateListener {
    void onNavigate(GenericFile path);
    void onNavigationCompleted(GenericFile path);
} public final class ActionModeController {

    private final MultiChoiceModeListener multiChoiceListener;

    private final Activity mActivity;

    private AbsListView mListView;

    private ActionMode mActionMode;
    private Intent mShareIntent;
    private int mShareItemsCount;

    public ActionModeController(final Activity activity) {
        this.mActivity = activity;
        this.multiChoiceListener = new MultiChoiceListener();
    }

    public void finishActionMode() {
        if (this.mActionMode != null) {
            this.mActionMode.finish();
        }
    }

    public void setListView(AbsListView list) {
        if (this.mActionMode != null) {
            this.mActionMode.finish();
        }
        this.mListView = list;
        this.mListView.setMultiChoiceModeListener(this.multiChoiceListener);
    }

    private final class MultiChoiceListener implements MultiChoiceModeListener {

        @Override
        public boolean onPrepareActionMode(ActionMode mode,
                                           Menu menu) {
            menu.clear();
            mActivity.getMenuInflater().inflate(
                    R.menu.browser_contextual, menu);

            final SparseBooleanArray items = mListView.getCheckedItemPositions();
            if (items != null) {
                final int checkedCount = mListView.getCheckedItemCount();

                final ArrayList<GenericFile> toShare = new ArrayList<>(checkedCount);
                for (int i = 0; i < items.size(); i++) {
                    final int key = items.keyAt(i);
                    if (items.get(key)) {
                        final GenericFile selected = (GenericFile) mListView.getItemAtPosition(key);
                        if (selected != null && !selected.isDirectory()) {
                            toShare.add(selected);
                        }
                    }
                }

                mShareItemsCount = toShare.size();
                if (toShare.isEmpty()) {
                    menu.removeItem(R.id.menu_share);
                } else if (toShare.size() == 1) {
                    mShareIntent = IntentsActionPolicy.createShareIntent(mActivity, toShare.get(0));
                } else {
                    mShareIntent = IntentsActionPolicy.createShareIntent(mActivity, toShare);
                }

                if (checkedCount > 1) {
                    menu.removeItem(android.R.id.edit);
                    menu.removeItem(R.id.properties);
                }
            }

            return true;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            ActionModeController.this.mActionMode = null;
        }

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            ActionModeController.this.mActionMode = mode;
            return true;
        }

        @Override
        public boolean onActionItemClicked(final ActionMode mode,
                                           final MenuItem item) {
            final SparseBooleanArray items = mListView
                    .getCheckedItemPositions();
            if (items != null) {
                final int checkedItemSize = items.size();
                final Resources res = mActivity.getResources();
                switch (item.getItemId()) {
                    case android.R.id.edit:
                        for (int i = 0; i < checkedItemSize; i++) {
                            final int key = items.keyAt(i);
                            if (items.get(key)) {
                                final RenameFileDialog rename = RenameFileDialog
                                        .instantiate(mode, (GenericFile) mListView
                                                .getItemAtPosition(key));
                                rename.show(mActivity.getFragmentManager(),
                                        BrowserPagerActivity.TAG_DIALOG);
                                return true;
                            }
                        }
                        return false;

                    case R.id.properties:
                        for (int i = 0; i < checkedItemSize; i++) {
                            final int key = items.keyAt(i);
                            if (items.get(key)) {
                                final GenericFile itemAtPosition = (GenericFile)
                                        mListView.getItemAtPosition(key);
                                if (itemAtPosition == null) {
                                    throw new RuntimeException("Item at position: " + key + "is null");
                                }
                                final FilePropertiesDialog prop = FilePropertiesDialog
                                        .newInstance(itemAtPosition);
                                mode.finish();
                                prop.show(mActivity.getFragmentManager(),
                                        BrowserPagerActivity.TAG_DIALOG);
                                break;
                            }
                        }
                        return true;

                    case R.id.menu_delete:
                        final List<GenericFile> files1 = new LinkedList<>();
                        final BrowserBaseAdapter adapter = (BrowserBaseAdapter) mListView.getAdapter();
                        for (int i = 0; i < checkedItemSize; i++) {
                            final int key = items.keyAt(i);
                            if (items.get(key)) {
                                files1.add(adapter.getItem(key));
                            }
                        }
                        final DeleteFilesDialog dialog = DeleteFilesDialog.newInstance(mode, files1);
                        dialog.show(mActivity.getFragmentManager(), BrowserPagerActivity.TAG_DIALOG);
                        return true;

                    case android.R.id.cut:
                        final GenericFile[] files = new GenericFile[mListView
                                .getCheckedItemCount()];
                        int index = -1;
                        for (int i = 0; i < checkedItemSize; i++) {
                            final int key = items.keyAt(i);
                            if (items.get(key)) {
                                files[++index] = (GenericFile) mListView.getItemAtPosition(key);
                            }
                        }
                        ClipBoard.cutMove(files);
                        Toast.makeText(
                                mActivity,
                                res.getQuantityString(
                                        R.plurals.cut_n_files_to_clipboard, index + 1, index + 1),
                                Toast.LENGTH_SHORT
                        ).show();
                        mode.finish();
                        return true;

                    case android.R.id.copy:
                        final GenericFile[] files2 = new GenericFile[mListView
                                .getCheckedItemCount()];
                        int index1 = -1;
                        for (int i = 0; i < checkedItemSize; i++) {
                            final int key = items.keyAt(i);
                            if (items.get(key)) {
                                files2[++index1] = (GenericFile) mListView.getItemAtPosition(key);
                            }
                        }
                        ClipBoard.cutCopy(files2);
                        Toast.makeText(
                                mActivity,
                                res.getQuantityString(R.plurals.copied_n_files_to_clipboard, index1 + 1,
                                        index1 + 1),
                                Toast.LENGTH_SHORT
                        ).show();
                        mode.finish();
                        return true;

                    case R.id.select_all:
                        for (int i = 0; i < mListView.getCount(); i++) {
                            mListView.setItemChecked(i, true);
                        }
                        return true;

                    case R.id.menu_add_to_media_store:
                        final String[] paths = new String[mListView.getCheckedItemCount()];
                        int pathsCounter = 0;
                        for (int i = 0; i < checkedItemSize; i++) {
                            final int key = items.keyAt(i);
                            if (items.get(key)) {
                                final GenericFile itemAtPosition = (GenericFile)
                                        mListView.getItemAtPosition(key);
                                if (itemAtPosition == null) {
                                    throw new RuntimeException("Item at position: " + key + "is null");
                                }
                                paths[pathsCounter++] = PFMFileUtils.fullPath(itemAtPosition);
                            }
                        }
                        MediaScannerConnection.scanFile(mActivity, paths, null, null);
                        mode.finish();
                        return true;

                    case R.id.menu_share:
                        if (mShareIntent != null) {
                            mActivity.startActivity(mShareIntent);
                        } else {
                            Toast.makeText(mActivity, res.getQuantityText(
                                            R.plurals.no_applications_can_share, mShareItemsCount),
                                    Toast.LENGTH_SHORT
                            ).show();
                        }
                        return true;

                    default:
                        return false;
                }
            }
            return false;
        }

        @Override
        public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
                                              boolean checked) {
            mode.setTitle(mActivity.getString(R.string._selected, mListView.getCheckedItemCount()));
            mode.invalidate();
        }
    }
} public final class MenuController {

    private final AbstractBrowserActivity mActivity;
    private final Browser mBrowser;

    private BrowserBaseAdapter mBrowserAdapter;

    public MenuController(@NonNull final AbstractBrowserActivity activity,
                          @NonNull final Browser browser) {
        this.mActivity = activity;
        this.mBrowser = browser;
    }

    public void setBrowserAdapter(@Nullable final BrowserBaseAdapter mBrowserAdapter) {
        this.mBrowserAdapter = mBrowserAdapter;
    }

    public boolean onMenuItemSelected(final MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                this.mBrowser.up();
                return true;

            case android.R.id.paste: {
                final GenericFile currentPath = mBrowser.getCurrentPath();
                if (currentPath != null) {
                    final PasteTaskExecutor ptc = new PasteTaskExecutor(mActivity, currentPath);
                    ptc.start();
                }
                return true;
            }

            case android.R.id.content:
                final Settings settings = Settings.getInstance(mActivity);
                switch (settings.getListAppearance()) {
                    case LIST:
                        settings.setListAppearance(Settings.ListAppearance.GRID);
                        break;

                    case GRID:
                        settings.setListAppearance(Settings.ListAppearance.LIST);
                        break;

                    default:
                        throw new IllegalArgumentException("Unexpected ListAppearance: " +
                        settings.getListAppearance());
                }
                mActivity.invalidateList();
                return true;

            case R.id.menu_search: {
                final GenericFile currentPath = mBrowser.getCurrentPath();
                if (currentPath != null) {
                    final Intent searchIntent = new Intent(mActivity, SearchActivity.class);
                    searchIntent.putExtra(Extras.EXTRA_FILE, currentPath);
                    mActivity.startActivity(searchIntent);
                }
                return true;
            }

            case R.id.menu_settings:
                mActivity.startActivityForResult(new Intent(mActivity, SettingsActivity.class),
                        AbstractBrowserActivity.REQUEST_CODE_SETTINGS);
                return true;

            case R.id.menu_folder_new: {
                final GenericFile currentPath = mBrowser.getCurrentPath();
                if (currentPath != null) {
                    final DialogFragment cd = CreateDirectoryDialog.instantiate(
                            mActivity, currentPath);
                    cd.show(mActivity.getFragmentManager(), AbstractBrowserActivity.TAG_DIALOG);
                }
                return true;
            }

            case R.id.menu_file_new: {
                final GenericFile currentPath = mBrowser.getCurrentPath();
                if (currentPath != null) {
                    final DialogFragment cf = CreateFileDialog.instantiate(
                            mActivity, currentPath);
                    cf.show(mActivity.getFragmentManager(), AbstractBrowserActivity.TAG_DIALOG);
                }
                return true;
            }

            case R.id.menu_partition: {
                final GenericFile currentPath = mBrowser.getCurrentPath();
                if (currentPath != null) {
                    final DialogFragment pid = PartitionInfoDialog.instantiate(currentPath);
                    pid.show(mActivity.getFragmentManager(), AbstractBrowserActivity.TAG_DIALOG);
                }
                return true;
            }

            case R.id.menu_sort_name_asc:
                mBrowserAdapter.setCompareType(FileSortType.NAME_ASC);
                return true;

            case R.id.menu_sort_name_desc:
                mBrowserAdapter.setCompareType(FileSortType.NAME_DESC);
                return true;

            case R.id.menu_sort_type_asc:
                mBrowserAdapter.setCompareType(FileSortType.TYPE_ASC);
                return true;

            case R.id.menu_sort_type_desc:
                mBrowserAdapter.setCompareType(FileSortType.TYPE_DESC);
                return true;

            case R.id.menu_sort_date_asc:
                mBrowserAdapter.setCompareType(FileSortType.DATE_ASC);
                return true;

            case R.id.menu_sort_date_desc:
                mBrowserAdapter.setCompareType(FileSortType.DATE_DESC);
                return true;

            case R.id.menu_sort_size_asc:
                mBrowserAdapter.setCompareType(FileSortType.SIZE_ASC);
                return true;

            case R.id.menu_sort_size_desc:
                mBrowserAdapter.setCompareType(FileSortType.SIZE_DESC);
                return true;

            default:
                return false;
        }
    }
} public final class FileFactory {
    private FileFactory() {}


    @NonNull
    public static GenericFile newFile(@NonNull final Settings settings,
                                      @NonNull final String path) {
        //if (Looper.myLooper() == Looper.getMainLooper()) {
            //throw new RuntimeException("Wrong thread");
        //} TODO check this
        return settings.useCommandLine() && Environment.hasBusybox() &&
                ShellHolder.getInstance().hasShell() ?
                        CommandLineFile.fromFile(settings, new File(path)) :
                                new JavaFile(path);
    }

    @NonNull
    public static GenericFile newFile(@NonNull final Settings settings,
                                      @NonNull final File path) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("Wrong thread");
        }
        return settings.useCommandLine() && Environment.hasBusybox() &&
                ShellHolder.getInstance().hasShell() ?
                        CommandLineFile.fromFile(settings, path) :
                                new JavaFile(path);
    }

    @NonNull
    public static GenericFile newFile(@NonNull final Settings settings,
                                      @NonNull final File parent,
                                      @NonNull final String name) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("Wrong thread");
        }
        return settings.useCommandLine() && Environment.hasBusybox() &&
                ShellHolder.getInstance().hasShell() ?
                        CommandLineFile.fromFile(settings, new File(parent, name)) :
                                new JavaFile(parent, name);
    }
} public interface GenericFile extends Serializable {

    /**
     * Creates a new, empty file on the file system according to the path
     * information stored in this file. This method returns true if it creates
     * a file, false if the file already existed. Note that it returns false
     * even if the file is not a file (because it's a directory, say).
     *
     * <p>Note that this method does <i>not</i> throw {@code IOException} if the file
     * already exists, even if it's not a regular file. Callers should always check the
     * return value
     *
     * @return true if the file has been created, false if it
     *         already exists.
     * @throws IOException if it's not possible to create the file.
     */
    boolean createNewFile() throws IOException;

    /**
     * Creates the directory named by this file, assuming its parents exist.
     * Use {@link #mkdirs} if you also want to create missing parents.
     *
     * <p>Note that this method does <i>not</i> throw {@code IOException} on failure.
     * Callers must check the return value. Note also that this method returns
     * false if the directory already existed. If you want to know whether the
     * directory exists on return, either use {@code (f.mkdir() || f.isDirectory())}
     * or simply ignore the return value from this method and simply call {@link #isDirectory}.
     *
     * @return {@code true} if the directory was created,
     *         {@code false} on failure or if the directory already existed.
     */
    boolean mkdir();

    /**
     * Creates the directory named by this file, creating missing parent
     * directories if necessary.
     * Use {@link #mkdir} if you don't want to create missing parents.
     *
     * <p>Note that this method does <i>not</i> throw {@code IOException} on failure.
     * Callers must check the return value. Note also that this method returns
     * false if the directory already existed. If you want to know whether the
     * directory exists on return, either use {@code (f.mkdirs() || f.isDirectory())}
     * or simply ignore the return value from this method and simply call {@link #isDirectory}.
     *
     * @return {@code true} if the directory was created,
     *         {@code false} on failure or if the directory already existed.
     */
    boolean mkdirs();

    /**
     * Force-deletes this file.
     *
     * <p>Note that this method does <i>not</i> throw {@code IOException} on failure.
     * Callers must check the return value.
     *
     * @return {@code true} if this file was deleted, {@code false} otherwise.
     */
    boolean delete();

    /**
     * Returns a boolean indicating whether this file can be found on the
     * underlying file system.
     *
     * @return {@code true} if this file exists, {@code false} otherwise.
     */
    boolean exists();

    /**
     * Indicates if this file represents a <em>directory</em> on the
     * underlying file system.
     *
     * @return {@code true} if this file is a directory, {@code false}
     *         otherwise.
     */
    boolean isDirectory();

    /**
     * Returns whether or not this file is a hidden file as defined by the
     * operating system. The notion of "hidden" is system-dependent. For Unix
     * systems a file is considered hidden if its name starts with a ".". For
     * Windows systems there is an explicit flag in the file system for this
     * purpose.
     *
     * @return {@code true} if the file is hidden, {@code false} otherwise.
     */
    boolean isHidden();

    /**
     * Determines whether the specified file is a Symbolic Link rather than an actual file.
     */
    boolean isSymlink();

    /**
     * Renames this file to {@code newPath}. This operation is supported for both
     * files and directories.
     *
     * <p>Many failures are possible. Some of the more likely failures include:
     * <ul>
     * <li>Write permission is required on the directories containing both the source and
     * destination paths.
     * <li>Search permission is required for all parents of both paths.
     * <li>Both paths be on the same mount point. On Android, applications are most likely to hit
     * this restriction when attempting to copy between internal storage and an SD card.
     * </ul>
     *
     * <p>Note that this method does <i>not</i> throw {@code IOException} on failure.
     * Callers must check the return value.
     *
     * @param newFile the new file.
     * @return true on success.
     */
    boolean renameTo(@NonNull GenericFile newFile);

    /**
     * Returns the length of this file in bytes.
     * Returns 0 if the file does not exist.
     * The result for a directory is not defined.
     *
     * @return the number of bytes in this file.
     */
    long length();

    /**
     * Returns the total length of this file in bytes.
     * Returns 0 if the file does not exist.
     * If this file is a directory, will return total directory size
     *
     * @return the number of bytes in this file or all containing file if is a directory
     */
    BigInteger lengthTotal();

    /**
     * Returns the time when this file was last modified, measured in
     * milliseconds since January 1st, 1970, midnight.
     * Returns 0 if the file does not exist.
     *
     * @return the time when this file was last modified.
     */
    long lastModified();

    /**
     * Returns the number of free bytes on the partition containing this path.
     * Returns 0 if this path does not exist.
     *
     * <p>Note that this is likely to be an optimistic over-estimate and should not
     * be taken as a guarantee your application can actually write this many bytes.
     *
     */
    long getFreeSpace();

    /**
     * Returns the total size in bytes of the partition containing this path.
     * Returns 0 if this path does not exist.
     */
    long getTotalSpace();

    /**
     * Returns permissions for the file
     *
     * @return permissions for the file
     */
    @NonNull
    Permissions getPermissions();

    /**
     * Applies permissions to the file
     *
     * @param newPerm Permissions to apply
     * @return true, if the permissions was applied
     */
    boolean applyPermissions(Permissions newPerm);

    /**
     * Returns path of this file.
     *
     * @return path of this file
     */
    @NonNull
    String getPath();

    /**
     * Returns mime type of this file.
     *
     * @return mime type of this file
     */
    @Nullable
    String getMimeType();

    /**
     * Returns the name of the file or directory represented by this file.
     *
     * @return this file's name or an empty string if there is no name part in
     *         the file's path.
     */
    @NonNull
    String getName();

    /**
     * Returns the absolute path of this file. An absolute path is a path that starts at a root
     * of the file system. On Android, there is only one root: {@code /}.
     *
     * <p>A common use for absolute paths is when passing paths to a {@code Process} as
     * command-line arguments, to remove the requirement implied by relative paths, that the
     * child must have the same working directory as its parent.
     */
    @NonNull
    String getAbsolutePath();

    /**
     * Returns the canonical path of this file.
     * An <i>absolute</i> path is one that begins at the root of the file system.
     * A <i>canonical</i> path is an absolute path with symbolic links
     * and references to "." or ".." resolved. If a path element does not exist (or
     * is not searchable), there is a conflict between interpreting canonicalization
     * as a textual operation (where "a/../b" is "b" even if "a" does not exist) .
     *
     * <p>Most callers should use {@link #getAbsolutePath} instead. A canonical path is
     * significantly more expensive to compute, and not generally useful. The primary
     * use for canonical paths is determining whether two paths point to the same file by
     * comparing the canonicalized paths.
     *
     * <p>It can be actively harmful to use a canonical path, specifically because
     * canonicalization removes symbolic links. It's wise to assume that a symbolic link
     * is present for a reason, and that that reason is because the link may need to change.
     * Canonicalization removes this layer of indirection. Good code should generally avoid
     * caching canonical paths.
     *
     * @return the canonical path of this file.
     * @throws IOException
     *             if an I/O error occurs.
     */
    @NonNull
    public String getCanonicalPath() throws IOException;

    /**
     * Returns a new file created using the canonical path of this file.
     * Equivalent to {@code new File(this.getCanonicalPath())}.
     *
     * @return the new file constructed from this file's canonical path.
     * @throws IOException
     *             if an I/O error occurs.
     */
    @NonNull
    public GenericFile getCanonicalFile() throws IOException;

    /**
     * Returns the pathname of the parent of this file. This is the path up to
     * but not including the last name. {@code null} is returned if there is no
     * parent.
     *
     * @return this file's parent pathname or {@code null}.
     */
    @Nullable
    String getParent();

    /**
     * Returns a new file made from the pathname of the parent of this file.
     * This is the path up to but not including the last name. {@code null} is
     * returned when there is no parent.
     *
     * @return a new file representing this file's parent or {@code null}.
     */
    @Nullable
    GenericFile getParentFile();


    /**
     * Returns java.io.File that represents this file
     *
     * @return java.io.File that represents this file
     */
    @NonNull
    File toFile();

    /**
     * Returns an array of files contained in the directory represented by this
     * file. The result is {@code null} if this file is not a directory. The
     * paths of the files in the array are absolute if the path of this file is
     * absolute, they are relative otherwise.
     *
     * @return an array of files or {@code null}.
     */
    @Nullable
    GenericFile[] listFiles();

    /**
     * Gets a list of the files in the directory represented by this file. This
     * list is then filtered through a FileFilter and matching files are
     * returned as an array of files. Returns {@code null} if this file is not a
     * directory. If {@code filter} is {@code null} then all files match.
     * <p>
     * The entries {@code .} and {@code ..} representing the current and parent
     * directories are not returned as part of the list.
     *
     * @param filter
     *            the filter to match names against, may be {@code null}.
     * @return an array of files or {@code null}.
     */
    @Nullable
    GenericFile[] listFiles(FileFilter filter);

    /**
     * Gets a list of the files in the directory represented by this file. This
     * list is then filtered through a FilenameFilter and files with matching
     * names are returned as an array of files. Returns {@code null} if this
     * file is not a directory. If {@code filter} is {@code null} then all
     * filenames match.
     * <p>
     * The entries {@code .} and {@code ..} representing the current and parent
     * directories are not returned as part of the list.
     *
     * @param filter
     *            the filter to match names against, may be {@code null}.
     * @return an array of files or {@code null}.
     */
    @Nullable
    GenericFile[] listFiles(FilenameFilter filter);

    /**
     * Gets a list of the files in the directory represented by this file. This
     * list is then filtered through a FileFilter and matching files are
     * returned as an array of files. Returns {@code null} if this file is not a
     * directory. If {@code filter} is {@code null} then all files match.
     * <p>
     * The entries {@code .} and {@code ..} representing the current and parent
     * directories are not returned as part of the list.
     *
     * @param filter
     *            the filter to match names against, may be {@code null}.
     * @return an array of files or {@code null}.
     */
    @Nullable
    GenericFile[] listFiles(GenericFileFilter filter);

    /**
     * Returns an array of strings with the file names in the directory
     * represented by this file. The result is {@code null} if this file is not
     * a directory.
     * <p>
     * The entries {@code .} and {@code ..} representing the current and parent
     * directory are not returned as part of the list.
     *
     * @return an array of strings with file names or {@code null}.
     */
    @Nullable
    String[] list();

    /**
     * Indicates whether the current context is allowed to read from this file.
     *
     * @return {@code true} if this file can be read, {@code false} otherwise.
     */
    boolean canRead();

    /**
     * Indicates whether the current context is allowed to write to this file.
     *
     * @return {@code true} if this file can be written, {@code false}
     *         otherwise.
     */
    boolean canWrite();

    /**
     * Tests whether or not this process is allowed to execute this file.
     * Note that this is a best-effort result; the only way to be certain is
     * to actually attempt the operation.
     *
     * @return {@code true} if this file can be executed, {@code false} otherwise.
     */
    boolean canExecute();
} public final class Settings {
    
    private static final String KEY_BOOKMARKS = "purefm.settings.keys.bookmarks";

    public enum ListAppearance {
        LIST, GRID
    }

    public enum Theme {
        DARK(R.style.ThemeDark), LIGHT(R.style.ThemeLight);

        @StyleRes
        public final int resId;

        private Theme(final int resId) {
            this.resId = resId;
        }
    }

    private static final Object LOCK = new Object();

    private static Settings sInstance;

    public static synchronized Settings getInstance(@NonNull final Context context) {
        synchronized (LOCK) {
            if (sInstance == null) {
                //noinspection ConstantConditions
                sInstance = new Settings(context.getApplicationContext());
            }
        }
        return sInstance;
    }

    public static Settings getInstance() {
        synchronized (LOCK) {
            if (sInstance == null) {
                throw new IllegalStateException(
                        "Settings was not yet initialized. Call getInstance(Context) first");
            }
            return sInstance;
        }
    }

    private final SharedPreferences mSharedPreferences;
    private final Resources mResources;

    private Theme mTheme;
    private ListAppearance mListAppearance;
    private boolean mListShowFileSize;
    private boolean mListShowHiddenFiles;
    private boolean mListShowPermissions;
    private boolean mListShowPreviews;
    private boolean mListShowModifiedDate;
    private boolean mUseCommandLine;
    private boolean mSuEnabled;
    private String mHomeDirectory;
    private Set<String> mBookmarks;
    
    private Settings(@NonNull final Context context) {
        final Resources res = context.getResources();
        mResources = res;
        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        mTheme = Theme.valueOf(mSharedPreferences.getString(
                res.getString(R.string.key_preference_theme), Theme.DARK.name()));

        mListAppearance = ListAppearance.valueOf(mSharedPreferences.getString(
                res.getString(R.string.key_preference_list_appearance), ListAppearance.LIST.name()));

        mListShowHiddenFiles = mSharedPreferences.getBoolean(
                res.getString(R.string.key_preference_list_show_hidden_files), false);

        mListShowFileSize = mSharedPreferences.getBoolean(
                res.getString(R.string.key_preference_list_show_size), true);

        mListShowPermissions = mSharedPreferences.getBoolean(
                res.getString(R.string.key_preference_list_show_permissions), true);

        mListShowPreviews = mSharedPreferences.getBoolean(
                res.getString(R.string.key_preference_list_show_preview), true);

        mListShowModifiedDate = mSharedPreferences.getBoolean(
                res.getString(R.string.key_preference_list_show_modified_date), true);

        mUseCommandLine = mSharedPreferences.getBoolean(
                res.getString(R.string.key_preference_use_commandline), false);

        mSuEnabled = mSharedPreferences.getBoolean(
                res.getString(R.string.key_preference_work_as_superuser), false);

        mHomeDirectory = mSharedPreferences.getString(
                res.getString(R.string.key_preference_home_directory),
                        android.os.Environment.getExternalStorageDirectory().getAbsolutePath());

        mBookmarks = mSharedPreferences.getStringSet(KEY_BOOKMARKS, new HashSet<String>());
    }
    
    public void setBookmarks(@NonNull final Set<String> bookmarks) {
        mBookmarks = bookmarks;
        mSharedPreferences.edit().putStringSet(KEY_BOOKMARKS, bookmarks).apply();
    }

    public void setTheme(final Theme theme, final boolean update) {
        mTheme = theme;
        if (update) {
            mSharedPreferences.edit().putString(
                    mResources.getString(R.string.key_preference_theme), theme.name()).apply();
        }
    }

    public Theme getTheme() {
        return mTheme;
    }

    public void setListAppearance(final ListAppearance appearance) {
        mListAppearance = appearance;
        mSharedPreferences.edit().putString(
                mResources.getString(R.string.key_preference_list_appearance),
                appearance.name()).apply();
    }

    public ListAppearance getListAppearance() {
        return mListAppearance;
    }

    public void setListShowFileSize(final boolean show, final boolean update) {
        mListShowFileSize = show;
        if (update) {
            mSharedPreferences.edit().putBoolean(
                    mResources.getString(R.string.key_preference_list_show_size), show).apply();
        }
    }

    public boolean listShowFileSizeEnabled() {
        return mListShowFileSize;
    }

    public void setListShowHiddenFiles(final boolean show, final boolean update) {
        mListShowHiddenFiles = show;
        if (update) {
            mSharedPreferences.edit().putBoolean(
                    mResources.getString(R.string.key_preference_list_show_hidden_files),
                            show).apply();
        }
    }

    public boolean listShowHiddenFilesEnabled() {
        return mListShowHiddenFiles;
    }

    public void setListShowPermissions(final boolean show, final boolean update) {
        mListShowPermissions = show;
        if (update) {
            mSharedPreferences.edit().putBoolean(
                    mResources.getString(R.string.key_preference_list_show_permissions),
                            show).apply();
        }
    }

    public boolean listShowPermissionsEnabled() {
        return mListShowPermissions;
    }

    public void setListShowPreviews(final boolean show, final boolean update) {
        mListShowPreviews = show;
        if (update) {
            mSharedPreferences.edit().putBoolean(
                    mResources.getString(R.string.key_preference_list_show_preview),
                            show).apply();
        }
    }

    public boolean listShowPreviewsEnabled() {
        return mListShowPreviews;
    }

    public void setListShowModifiedDate(final boolean show, final boolean update) {
        mListShowModifiedDate = show;
        if (update) {
            mSharedPreferences.edit().putBoolean(
                    mResources.getString(R.string.key_preference_list_show_modified_date),
                            show).apply();
        }
    }

    public boolean listShowModifiedDateEnabled() {
        return mListShowModifiedDate;
    }

    public void setUseCommandLine(final boolean use, final boolean update) {
        mUseCommandLine = use;
        if (update) {
            mSharedPreferences.edit().putBoolean(
                    mResources.getString(R.string.key_preference_use_commandline), use).apply();
        }
    }

    public boolean useCommandLine() {
        return mUseCommandLine;
    }

    public void setSuEnabled(final boolean enabled, final boolean update) {
        mSuEnabled = enabled;
        if (update) {
            mSharedPreferences.edit().putBoolean(
                    mResources.getString(R.string.key_preference_work_as_superuser), enabled).apply();
        }
    }

    public boolean isSuEnabled() {
        return mSuEnabled;
    }

    public void setHomeDirectory(final String path, final boolean update) {
        mHomeDirectory = path;
        if (update) {
            mSharedPreferences.edit().putString(
                    mResources.getString(R.string.key_preference_home_directory), path).apply();
        }
    }

    public String getHomeDirectory() {
        return mHomeDirectory;
    }

    public Set<String> getBookmarks() {
        return mBookmarks;
    }
} public final class DirectoryScanTask extends
        AsyncTask<GenericFile, Void, GenericFile[]> {

    @NonNull
    private final BrowserBaseAdapter mBrowserAdapter;

    @NonNull
    private final Browser mBrowser;

    @NonNull
    private final SwipeRefreshLayout[] mSwipeRefreshLayouts;

    private final ListFileFilter mFileFilter;
    
    private GenericFile mFile;

    public DirectoryScanTask(@NonNull final Browser browser,
                             @Nullable final String mimeType,
                             @NonNull final BrowserBaseAdapter adapter,
                             @NonNull final Settings settings,
                             @NonNull final SwipeRefreshLayout... refreshLayouts) {
        mBrowser = browser;
        mSwipeRefreshLayouts = refreshLayouts;
        mBrowserAdapter = adapter;
        mFileFilter = new ListFileFilter(settings.listShowHiddenFilesEnabled());
        mFileFilter.setMimeType(mimeType);
    }

    @Override
    protected void onPreExecute() {
        for (final SwipeRefreshLayout layout : mSwipeRefreshLayouts) {
            layout.setRefreshing(true);
        }
    }

    @Override
    protected GenericFile[] doInBackground(GenericFile... arg0) {
        this.mFile = arg0[0];
        return arg0[0].listFiles(mFileFilter);
    }

    @Override
    protected void onPostExecute(GenericFile[] result) {
        super.onPostExecute(result);
        mBrowserAdapter.updateData(result);
        mBrowser.onScanFinished(this.mFile);
        for (final SwipeRefreshLayout layout : mSwipeRefreshLayouts) {
            layout.setRefreshing(false);
        }
    }

    @Override
    protected void onCancelled(GenericFile[] result) {
        super.onCancelled(result);
        mBrowser.onScanCancelled(false);
        for (final SwipeRefreshLayout layout : mSwipeRefreshLayouts) {
            layout.setRefreshing(false);
        }
    }

    private static final class ListFileFilter implements GenericFileFilter {

        private String mType;
        private boolean mAcceptAll;

        private final boolean mShowHidden;

        ListFileFilter(final boolean showHidden) {
            mShowHidden = showHidden;
        }
        
        void setMimeType(String type) {
            this.mType = type;
            this.mAcceptAll = type == null || type.equals("*/*");
        }

        @Override
        public boolean accept(final GenericFile pathname) {
            if (!mShowHidden && pathname.isHidden()) {
                return false;
            }
            if (this.mType == null) {
                return true;
            }
            if (this.mAcceptAll || pathname.isDirectory()) {
                return true;
            }
            final String fileType = pathname.getMimeType();
            return fileType != null
                    && MimeTypes.mimeTypeMatch(this.mType, fileType);
        }
    }

} public abstract class AbstractBrowserActivity extends ActionBarIconThemableActivity
        implements Browser.OnNavigateListener {

    public static final int REQUEST_CODE_SETTINGS = 0;

    public static final String TAG_DIALOG = "dialog";


    @Override
    public void onNavigate(GenericFile path) {

    }

    @Override
    public void onBackPressed() {
        final boolean onFragmentBackPressed;
        final BrowserFragment fragment = getCurrentlyDisplayedFragment();
            onFragmentBackPressed = fragment != null
                && fragment.onBackPressed();

        if (!onFragmentBackPressed) {
            onBackPressedConfirmed();
        }
    }

    /**
     * Returns currently displayed BrowserFragment
     *
     * @return currently displayed BrowserFragment
     */
    @Nullable
    protected abstract BrowserFragment getCurrentlyDisplayedFragment();

    /**
     * Sets currently displayed fragment
     *
     * @param fragment BrowserFragment to set
     */
    public abstract void setCurrentlyDisplayedFragment(BrowserFragment fragment);

    /**
     * Set OnSequenceClickListener to BreadCrumbTextView
     *
     * @param sequenceListener OnSequenceListener to set
     */
    public abstract void setOnSequenceClickListener(BreadCrumbTextView.OnSequenceClickListener sequenceListener);

    /**
     * Returns mime type if GET_CONTENT mode is set
     *
     * @return mime type if GET_CONTENT mode is set
     */
    public String getGetContentMimeType() {
        return null;
    }

    /**
     * Perform action if onBackPressed from BrowserFragment confirmed
     */
    protected void onBackPressedConfirmed() {
        finish();
    }

    /**
     * Returns true if BrowserFragment can show it's menu
     *
     * @return true if BrowserFragment can show it's menu
     */
    public abstract boolean shouldShowBrowserFragmentMenu();

    /**
     * Refresh content list
     */
    public abstract void invalidateList();

    /**
     * Should return true if history is enabled
     */
    public abstract boolean isHistoryEnabled();
} public final class ClipBoard {

    private static final Object LOCK = new Object();

    /**
     * Files in clip board
     */
    private static GenericFile[] sClipBoard;
    
    /**
     * True if move, false if cutCopy
     */
    private static boolean sIsMove;

    /**
     * If true, clipBoard can't be modified
     */
    private static volatile boolean sIsLocked;
    
    private ClipBoard() {}
    
    public static synchronized boolean cutCopy(@NonNull final GenericFile[] files) {
        synchronized (LOCK) {
            if (!sIsLocked) {
                sIsMove = false;
                sClipBoard = files;
                return true;
            } else {
                Log.w("ClipBoard", "Trying to cutCopy but clipboard is locked");
                return false;
            }
        }
    }
    
    public static synchronized boolean cutMove(@NonNull final GenericFile[] files) {
        synchronized (LOCK) {
            if (!sIsLocked) {
                sIsMove = true;
                sClipBoard = files;
                return true;
            } else {
                Log.w("ClipBoard", "Trying to cutMove clipboard is locked");
                return false;
            }
        }
    }

    public static void lock() {
        synchronized (LOCK) {
            sIsLocked = true;
        }
    }

    public static void unlock() {
        synchronized (LOCK) {
            sIsLocked = false;
        }
    }

    @Nullable
    public static GenericFile[] getClipBoardContents() {
        synchronized (LOCK) {
            return sClipBoard;
        }
    }
    
    public static boolean isEmpty() {
        synchronized (LOCK) {
            return sClipBoard == null;
        }
    }
    
    public static boolean isMove() {
        synchronized (LOCK) {
            return sIsMove;
        }
    }
    
    public static boolean clear() {
        synchronized (LOCK) {
            if (!sIsLocked) {
                sClipBoard = null;
                sIsMove = false;
                return true;
            } else {
                Log.w("ClipBoard", "Trying to clear, but ClipBoard is locked");
                return false;
            }
        }
    }
    
} public final class PFMFileUtils {

    private PFMFileUtils() {
    }

    public static final class FileNameFilter implements InputFilter {

        public static final int MAX_FILENAME_LENGTH = 255;

        private static final String FILENAME_PATTERN =
                "\\\\|/|:|\\*|\\?|\"|<|>|\r|\n";

        @Nullable
        @Override
        public CharSequence filter(final CharSequence source,
                                   final int start,
                                   final int end,
                                   final Spanned dest,
                                   final int dstart,
                                   final int dend) {
            if (source.length() == 0) {
                //nothing to filter
                return null;
            }
            final CharSequence sourceSubSequence = source.subSequence(start, end);
            final String sourceSubSequenceString = sourceSubSequence.toString();
            final String sourceProcessed = sourceSubSequenceString.replaceAll(FILENAME_PATTERN, "");
            if (sourceSubSequenceString.equals(sourceProcessed)) {
                // nothing filtered
                return null;
            }
            if (source instanceof Spanned) {
                // copy spans from original source
                final Spannable processed = new SpannableString(sourceProcessed);
                TextUtils.copySpansFrom((Spanned) source, start, processed.length(), null, processed, 0);
                return processed;
            }
            return sourceProcessed;
        }
    }

    /**
     * Returns canonical path or absolute path if failed
     *
     * @param file File to get full path
     * @return canonical path or absolute path if failed
     */
    @NonNull
    public static String fullPath(@NonNull final GenericFile file) {
        try {
            return file.getCanonicalPath();
        } catch (IOException e) {
            return file.getAbsolutePath();
        }
    }

    /**
     * Returns canonical path or absolute path if failed
     *
     * @param file File to get full path
     * @return canonical path or absolute path if failed
     */
    @NonNull
    public static String fullPath(@NonNull final File file) {
        try {
            return file.getCanonicalPath();
        } catch (IOException e) {
            return file.getAbsolutePath();
        }
    }

    @NonNull
    public static String byteCountToDisplaySize(@NonNull final BigInteger size) {
        String displaySize;

        if (size.divide(FileUtils.ONE_GB_BI).compareTo(BigInteger.ZERO) > 0) {
            displaySize = String.valueOf(size.divide(FileUtils.ONE_GB_BI)) + " GiB";
        } else if (size.divide(FileUtils.ONE_MB_BI).compareTo(BigInteger.ZERO) > 0) {
            displaySize = String.valueOf(size.divide(FileUtils.ONE_MB_BI)) + " MiB";
        } else if (size.divide(FileUtils.ONE_KB_BI).compareTo(BigInteger.ZERO) > 0) {
            displaySize = String.valueOf(size.divide(FileUtils.ONE_KB_BI)) + " KiB";
        } else {
            displaySize = String.valueOf(size) + " B";
        }
        return displaySize;
    }

    @Nullable
    public static String resolveFileSystem(@NonNull final GenericFile file) {
        final String path = PFMFileUtils.fullPath(file);
        return resolveFileSystem(FilenameUtils.getFullPathNoEndSeparator(path));
    }


    @Nullable
    public static String resolveFileSystem(@NonNull final String path) {
        for (final StorageHelper.Volume v : Environment.getVolumes()) {
            if (path.startsWith(v.file.getAbsolutePath())) {
                return v.fileSystem;
            }
        }

        if (!ShellHolder.getInstance().hasShell()) {
            Log.w("resolveFileSystem()", "no shell, aborting");
            return null;
        }

        final List<String> fsTypeResult = CommandLine.executeForResult(new CommandStat(path));
        return fsTypeResult == null || fsTypeResult.isEmpty() ?
                null : fsTypeResult.get(0);
    }
    
    public static void openFileInExternalApp(@NonNull final Context context,
                                             @NonNull final File target) {
        final String mime = MimeTypes.getMimeType(target);
        if (mime != null) {
            final Intent i = new Intent(Intent.ACTION_VIEW);
            i.setDataAndType(Uri.fromFile(target), mime);
            final PackageManager packageManager = context.getPackageManager();
            if (packageManager == null) {
                throw new IllegalArgumentException("No PackageManager for context");
            }
            if (packageManager.queryIntentActivities(i, 0).isEmpty()) {
                Toast.makeText(context, R.string.no_apps_to_open, Toast.LENGTH_SHORT).show();
                return;
            }
            try {
                context.startActivity(i);
            } catch (Exception e) {
                Toast.makeText(context, context.getString(R.string.could_not_open_file_) + e.getMessage(), Toast.LENGTH_SHORT).show();
            }
        }
    }

    public static void moveFile(@NonNull final GenericFile source,
                                @NonNull final GenericFile target,
                                final boolean useCommandLine) throws IOException {
        if (useCommandLine) {
            if (target.exists()) {
                throw new FileExistsException("Target exists");
            }
            final boolean result = CommandLine.execute(new CommandMove(source, target));
            if (!result) {
                throw new IOException("Move failed");
            }
        } else {
            FileUtils.moveFile(source.toFile(), target.toFile());
        }
    }

    public static void copyFile(@NonNull final GenericFile source,
                                @NonNull final GenericFile target,
                                final boolean useCommandLine) throws IOException {
        if (useCommandLine) {
            final boolean result = CommandLine.execute(new CommandCopyRecursively(source, target));
            if (!result) {
                throw new IOException("Move failed");
            }
        } else {
            FileUtils.copyFile(source.toFile(), target.toFile());
        }
    }

    public static void copyFileToDirectory(@NonNull final GenericFile source,
                                           @NonNull final GenericFile target,
                                           final boolean useCommandLine) throws IOException {
        if (useCommandLine) {
            if (!target.exists()) {
                throw new FileNotFoundException("Target doesn't exist");
            }
            if (!target.isDirectory()) {
                throw new IllegalArgumentException("Target is not a directory");
            }
            final boolean result = CommandLine.execute(new CommandCopyRecursively(source, target));
            if (!result) {
                throw new IOException("Move failed");
            }
        } else {
            FileUtils.copyFileToDirectory(source.toFile(), target.toFile());
        }
    }

    public static void moveDirectory(@NonNull final GenericFile source,
                                     @NonNull final GenericFile target,
                                     final boolean useCommandLine) throws IOException {
        if (useCommandLine) {
            if (target.exists()) {
                throw new FileExistsException("Target exists");
            }
            final boolean result = CommandLine.execute(new CommandMove(source, target));
            if (!result) {
                throw new IOException("Move failed");
            }
        } else {
            FileUtils.moveDirectory(source.toFile(), target.toFile());
        }
    }

    public static void moveToDirectory(@NonNull final GenericFile source,
                                       @NonNull final GenericFile target,
                                       final boolean useCommandLine,
                                       final boolean createDestDir) throws IOException {

        if (useCommandLine) {
            if (!source.exists()) {
                throw new FileNotFoundException("Source '" + source + "' does not exist");
            }
            if (target.exists()) {
                if (!target.isDirectory()) {
                    throw new IOException("Target is not a directory");
                }
            } else {
                if (createDestDir) {
                    if (!target.mkdirs()) {
                        throw new IOException("Failed to create target directory");
                    }
                } else {
                    throw new FileNotFoundException("Target directory doesn't exist");
                }
            }
            final boolean result = CommandLine.execute(new CommandMove(source, target));
            if (!result) {
                throw new IOException("Moving failed");
            }
        } else {
            FileUtils.moveToDirectory(source.toFile(), target.toFile(), createDestDir);
        }
    }

    public static void copyDirectory(@NonNull final GenericFile source,
                                     @NonNull final GenericFile target,
                                     final boolean useCommandLine) throws IOException {
        if (useCommandLine) {
            if (!source.exists()) {
                throw new FileNotFoundException("Source '" + source + "' does not exist");
            }
            if (!source.isDirectory()) {
                throw new IOException("Source '" + source + "' exists but is not a directory");
            }
            if (source.getCanonicalPath().equals(target.getCanonicalPath())) {
                throw new IOException("Source '" + source + "' and destination '" + target +
                        "' are the same");
            }
            final boolean result = CommandLine.execute(new CommandCopyRecursively(source, target));
            if (!result) {
                throw new IOException("Copying failed");
            }
        } else {
            FileUtils.copyDirectory(source.toFile(), target.toFile());
        }
    }

    public static void copyDirectoryToDirectory(@NonNull final GenericFile source,
                                                @NonNull final GenericFile target,
                                                final boolean useCommandLine) throws IOException {
        if (useCommandLine) {
            if (!source.exists()) {
                throw new FileNotFoundException("Source '" + source + "' does not exist");
            }
            if (!source.isDirectory()) {
                throw new IOException("Source '" + source + "' exists but is not a directory");
            }
            if (source.getCanonicalPath().equals(target.getCanonicalPath())) {
                throw new IOException("Source '" + source + "' and destination '" + target +
                        "' are the same");
            }
            if (!target.exists()) {
                throw new FileNotFoundException("Target dir does not exist");
            }
            if (!target.isDirectory()) {
                throw new IOException("Target is not a directory");
            }
            final boolean result = CommandLine.execute(new CommandCopyRecursively(source, target));
            if (!result) {
                throw new IOException("Copying failed");
            }
        } else {
            FileUtils.copyDirectoryToDirectory(source.toFile(), target.toFile());
        }
    }

    public static void forceDelete(@NonNull final GenericFile file,
                                   final boolean useCommandLine) throws IOException {
        if (useCommandLine) {
            if (!file.exists()) {
                throw new FileNotFoundException("File does not exist: " + file);
            }
            final boolean result = CommandLine.execute(new CommandRemove(file.toFile()));
            if (!result) {
                throw new IOException("Removing failed");
            }
        } else {
            FileUtils.forceDelete(file.toFile());
        }
    }
    
    public static final class NameComparator implements Comparator<GenericFile> {
        
        @Override
        public int compare(final GenericFile a, final GenericFile b) {
            if (a.isDirectory() && b.isDirectory()) {
                return String.CASE_INSENSITIVE_ORDER.compare(a.getName(), b.getName());
            }
            
            if (a.isDirectory()) {
                return -1;
            }
            
            if (b.isDirectory()) {
                return 1;
            }
            
            return a.getName().compareTo(b.getName());
        }
    }
    
    public static final class NameComparatorReverse implements Comparator<GenericFile> {
        
        @Override
        public int compare(final GenericFile a, final GenericFile b) {
            if (a.isDirectory() && b.isDirectory()) {
                final int res = String.CASE_INSENSITIVE_ORDER.compare(a.getName(), b.getName());
                if (res == 0) {
                    return 0;
                }
                if (res < 0) {
                    return 1;
                }
                return -1;
            }
            
            if (a.isDirectory()) {
                return -1;
            }
            
            if (b.isDirectory()) {
                return 1;
            }
            
            final int res = a.getName().compareTo(b.getName());
            if (res == 0) {
                return 0;
            }
            if (res < 0) {
                return 1;
            }
            return -1;
        }
    }
    
    public static final class DateComparatorAsc implements Comparator<GenericFile> {
        
        @Override
        public int compare(final GenericFile a, final GenericFile b) {
            final long date_a = a.lastModified();
            final long date_b = b.lastModified();
            
            if (date_a == date_b) {
                return 0;
            }
            
            if (date_a < date_b) {
                return -1;
            }
            
            return 1;
        }
    }
    
    public static final class DateComparatorDesc implements Comparator<GenericFile> {
        
        @Override
        public int compare(final GenericFile a, final GenericFile b) {
            final long date_a = a.lastModified();
            final long date_b = b.lastModified();
            
            if (date_a == date_b) {
                return 0;
            }
            
            if (date_a < date_b) {
                return 1;
            }
            
            return -1;
        }
    }
    
    public static final class SizeComparatorAsc implements Comparator<GenericFile> {
        
        @Override
        public int compare(final GenericFile a, final GenericFile b) {
            if (a.isDirectory() && b.isDirectory()) {
                return a.getName().compareTo(b.getName());
            }
            
            if (a.isDirectory()) {
                return -1;
            }
            
            if (b.isDirectory()) {
                return 1;
            }
            
            final long len_a = a.length();
            final long len_b = b.length();
            
            if (len_a == len_b) {
                return a.getName().compareTo(b.getName());
            }
            
            if (len_a < len_b) {
                return -1;
            }
            
            return 1;
        }
    }
    
    public static final class SizeComparatorDesc implements Comparator<GenericFile> {
        
        @Override
        public int compare(final GenericFile a, final GenericFile b) {
            if (a.isDirectory() && b.isDirectory()) {
                return a.getName().compareTo(b.getName());
            }
            
            if (a.isDirectory()) {
                return 1;
            }
            
            if (b.isDirectory()) {
                return -1;
            }
            
            final long len_a = a.length();
            final long len_b = b.length();
            
            if (len_a == len_b) {
                return a.getName().compareTo(b.getName());
            }
            
            if (len_a < len_b) {
                return 1;
            }
            
            return -1;
        }
    }
    
    public static final class TypeComparator implements Comparator<GenericFile> {
        
        @Override
        public int compare(final GenericFile a, final GenericFile b) {
            
            if (a.isDirectory() && b.isDirectory()) {
                return a.getName().compareTo(b.getName());
            }
            
            if (a.isDirectory()) {
                return -1;
            }
            
            if (b.isDirectory()) {
                return 1;
            }
            
            final String ext_a = FilenameUtils.getExtension(a.getName());
            final String ext_b = FilenameUtils.getExtension(b.getName());
            
            if (ext_a.isEmpty() && ext_b.isEmpty()) {
                return a.getName().compareTo(b.getName());
            }
            
            if (ext_a.isEmpty()) {
                return -1;
            }
            
            if (ext_b.isEmpty()) {
                return 1;
            }
            
            final int res = ext_a.compareTo(ext_b);
            if (res == 0) {
                return a.getName().compareTo(b.getName());
            }
            return res;
        }
    }
    
    public static final class TypeComparatorReverse implements Comparator<GenericFile> {
        
        @Override
        public int compare(final GenericFile a, final GenericFile b) {
            if (a.isDirectory() && b.isDirectory()) {
                return a.getName().compareTo(b.getName());
            }
            
            if (a.isDirectory()) {
                return -1;
            }
            
            if (b.isDirectory()) {
                return 1;
            }
            
            final String ext_a = FilenameUtils.getExtension(a.getName());
            final String ext_b = FilenameUtils.getExtension(b.getName());
            
            if (ext_a.isEmpty() && ext_b.isEmpty()) {
                return a.getName().compareTo(b.getName());
            }
            
            if (ext_a.isEmpty()) {
                return 1;
            }
            
            if (ext_b.isEmpty()) {
                return -1;
            }
            
            final int res = ext_a.compareTo(ext_b);
            if (res == 0) {
                return a.getName().compareTo(b.getName());
            }
            return res;
        }
    }
} public final class ThemeUtils {
    private ThemeUtils() {}

    /**
     * Returns an int for attr from the {@link Resources.Theme}
     *
     * @param theme {@link Resources.Theme} to get int from
     * @param attr Attribute of the int
     * @return dimension for attr from the {@link Resources.Theme}
     */
    public static int getInteger(@NonNull final Resources.Theme theme, final int attr) {
        final TypedArray array = theme.obtainStyledAttributes(new int[] {attr});
        try {
            return array.getInteger(0, 0);
        } finally {
            array.recycle();
        }
    }

    /**
     * Returns an int for attr from the {@link Resources.Theme}
     *
     * @param theme {@link Resources.Theme} to get int from
     * @param attr Attribute of the int
     * @param defaultValue value to return if not found
     * @return dimension for attr from the {@link Resources.Theme}
     */
    public static int getInteger(@NonNull final Resources.Theme theme,
                                 final int attr,
                                 final int defaultValue) {
        final TypedArray array = theme.obtainStyledAttributes(new int[] {attr});
        try {
            return array.getInteger(0, defaultValue);
        } finally {
            array.recycle();
        }
    }

    /**
     * Returns a dimension for attr from the {@link Resources.Theme}
     *
     * @param theme {@link Resources.Theme} to get dimension from
     * @param attr Attribute of the dimension
     * @return dimension for attr from the {@link Resources.Theme}
     */
    public static float getDimension(@NonNull final Resources.Theme theme, final int attr) {
        final TypedArray array = theme.obtainStyledAttributes(new int[] {attr});
        try {
            return array.getDimension(0, 0f);
        } finally {
            array.recycle();
        }
    }

    /**
     * Returns a {@link Drawable} for attr from the {@link Resources.Theme},
     * or null if not found
     *
     * @param theme {@link Resources.Theme} to get {@link Drawable} from
     * @param attr Attribute of the {@link Drawable}
     * @return {@link Drawable} for attr from the {@link Resources.Theme}
     */
    @Nullable
    public static Drawable getDrawable(@NonNull final Resources.Theme theme, final int attr) {
        final TypedArray array = theme.obtainStyledAttributes(new int[] {attr});
        try {
            return array.getDrawable(0);
        } finally {
            array.recycle();
        }
    }

    /**
     * Returns a {@link Drawable} for attr from the {@link Resources.Theme}.
     *
     * @param theme {@link Resources.Theme} to get {@link Drawable} from
     * @param attr Attribute of the {@link Drawable}
     * @throws RuntimeException if the {@link Drawable} is not found
     * @return {@link Drawable} for attr from the  {@link Resources.Theme}
     */
    @NonNull
    public static Drawable getDrawableNonNull(@NonNull final Resources.Theme theme,
                                              final int attr) {
        final TypedArray array = theme.obtainStyledAttributes(new int[] {attr});
        try {
            final Drawable drawable = array.getDrawable(0);
            if (drawable == null) {
                throw new RuntimeException(
                        "Drawable for attr \'" + attr + "\' not found in " + theme);
            }
            return drawable;
        } finally {
            array.recycle();
        }
    }

    /**
     * Returns a {@link Drawable} for attr from the {@link ContextThemeWrapper}
     * Throws {@link RuntimeException} if the {@link Drawable} not found
     *
     * @param themeWrapper {@link ContextThemeWrapper} to get {@link Drawable} from
     * @param attr Attribute of the {@link Drawable}
     * @throws RuntimeException if the {@link Drawable} is not found
     * @return {@link Drawable} for attr from the {@link ContextThemeWrapper}
     */
    @NonNull
    public static Drawable getDrawableNonNull(@NonNull final ContextThemeWrapper themeWrapper,
                                              final int attr) {
        return getDrawableNonNull(themeWrapper.getTheme(), attr);
    }
} public interface OnSequenceClickListener {
    void onSequenceClick(String sequence);
}
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListView;
import com.cyanogenmod.filemanager.util.MediaHelper;
import com.docd.purefm.R;
import com.docd.purefm.adapters.BrowserBaseAdapter;
import com.docd.purefm.adapters.BrowserGridAdapter;
import com.docd.purefm.adapters.BrowserListAdapter;
import com.docd.purefm.browser.Browser;
import com.docd.purefm.browser.Browser.OnNavigateListener;
import com.docd.purefm.controller.ActionModeController;
import com.docd.purefm.controller.MenuController;
import com.docd.purefm.file.FileFactory;
import com.docd.purefm.file.GenericFile;
import com.docd.purefm.settings.Settings;
import com.docd.purefm.tasks.DirectoryScanTask;
import com.docd.purefm.ui.activities.AbstractBrowserActivity;
import com.docd.purefm.utils.ClipBoard;
import com.docd.purefm.utils.PFMFileUtils;
import com.docd.purefm.utils.ThemeUtils;
import com.docd.purefm.ui.view.BreadCrumbTextView.OnSequenceClickListener;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.File;
/*
 * Copyright 2014 Yaroslav Mytkalyk
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.docd.purefm.ui.fragments;






/**
 * Fragment manages file List, menu and ActionMode.
 * @author Doctoror
 */
public final class BrowserFragment extends UserVisibleHintFragment
        implements SwipeRefreshLayout.OnRefreshListener {

    private static final String STATE_BROWSER = "BrowserFragment.state.mBrowser";

    //private ActionBar actionBar;
    private ActionModeController actionModeController;
    private MenuController menuController;

    private Browser mBrowser;
    private BrowserBaseAdapter mAdapter;