com.docd.purefm.adapters.BrowserBaseAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.docd.purefm.adapters.BrowserBaseAdapter.java

Source

/*
 * 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.adapters;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.app.Activity;
import android.content.res.Resources;
import android.database.DataSetObservable;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.FileObserver;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.LayoutInflater;
import android.widget.ImageView;
import android.widget.ListAdapter;

import com.docd.purefm.R;
import com.docd.purefm.file.FileFactory;
import com.docd.purefm.file.FileObserverCache;
import com.docd.purefm.file.GenericFile;
import com.docd.purefm.file.MultiListenerFileObserver;
import com.docd.purefm.file.Permissions;
import com.docd.purefm.settings.Settings;
import com.docd.purefm.utils.DrawableLruCache;
import com.docd.purefm.utils.FileSortType;
import com.docd.purefm.utils.MimeTypes;
import com.docd.purefm.utils.PFMFileUtils;
import com.docd.purefm.utils.PreviewHolder;
import com.docd.purefm.utils.ThemeUtils;
import com.docd.purefm.ui.view.OverlayImageView;

import org.apache.commons.io.FilenameUtils;
import android.support.annotation.NonNull;

/**
 * Base adapter for file list.
 * Manages FileObserver events
 * @author Doctoror
 */
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);
                    }
                });
            }
        }
    }

}