/*
* 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);
}
});
}
}
}
}