/*
* 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.browser;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Environment;
import android.os.FileObserver;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import android.widget.Toast;
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.settings.Settings;
import com.docd.purefm.ui.activities.AbstractBrowserActivity;
import com.docd.purefm.utils.PFMFileUtils;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Browser manages current path and navigation
* @author Doctoror
*/
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);
}
}
}
}