package com.openfarmanager.android.fragments;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.text.Layout;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.openfarmanager.android.App;
import com.openfarmanager.android.R;
import com.openfarmanager.android.adapters.LinesAdapter;
import com.openfarmanager.android.filesystem.actions.RootTask;
import com.openfarmanager.android.model.ViewerBigFileTextViewer;
import com.openfarmanager.android.model.ViewerTextBuffer;
import com.openfarmanager.android.model.exeptions.SdcardPermissionException;
import com.openfarmanager.android.utils.FileUtilsExt;
import com.openfarmanager.android.utils.ReversedIterator;
import com.openfarmanager.android.dialogs.QuickPopupDialog;
import com.openfarmanager.android.dialogs.SelectEncodingDialog;
import com.openfarmanager.android.utils.StorageUtils;
import com.openfarmanager.android.utils.SystemUtils;
import com.openfarmanager.android.view.ToastNotification;
import org.apache.commons.io.IOCase;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import rx.Observable;
import rx.Subscriber;
import rx.Subscription;
import rx.schedulers.Schedulers;
import static com.openfarmanager.android.controllers.EditViewController.MSG_BIG_FILE;
import static com.openfarmanager.android.controllers.EditViewController.MSG_TEXT_CHANGED;
import static com.openfarmanager.android.utils.Extensions.getThreadPool;
import static com.openfarmanager.android.utils.StorageUtils.checkForPermissionAndGetBaseUri;
import static com.openfarmanager.android.utils.StorageUtils.checkUseStorageApi;
/**
* File viewer
*/
public class Viewer extends Fragment {
private static final String TAG = "::: Viewer :::";
private static final int MB = 1048576;
public static final int MAX_FILE_SIZE = 3 * MB;
public static final int LINES_COUNT_FRAGMENT = 2000;
private File mFile;
private ListView mList;
private LinesAdapter mAdapter;
private ProgressBar mProgress;
private Handler mHandler;
private ViewerTextBuffer mText;
private ViewerBigFileTextViewer mBigText;
private boolean mStopLoading;
private boolean mBigFile;
private LoadFileTask mLoadFileTask;
private LoadTextFragmentTask mLoadTextFragmentTask;
private Charset mSelectedCharset = Charset.forName("UTF-8");
private Dialog mCharsetSelectDialog;
protected QuickPopupDialog mSearchResultsPopup;
private int mCalculatedRowHeight = -1;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.viewer, container);
mList = (ListView) view.findViewById(android.R.id.list);
mProgress = (ProgressBar) view.findViewById(android.R.id.progress);
mText = new ViewerTextBuffer();
mBigText = new ViewerBigFileTextViewer();
view.findViewById(R.id.root_view).setBackgroundColor(App.sInstance.getSettings().getViewerColor());
mSearchResultsPopup = new QuickPopupDialog(getActivity(), view, R.layout.search_results_popup);
mSearchResultsPopup.setPosition(Gravity.RIGHT | Gravity.TOP, (int) (50 * getResources().getDisplayMetrics().density));
return view;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (mCharsetSelectDialog != null && mCharsetSelectDialog.isShowing()) {
adjustDialogSize(mCharsetSelectDialog);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == BaseFileSystemPanel.REQUEST_CODE_REQUEST_PERMISSION && Build.VERSION.SDK_INT >= 21 && data != null) {
getActivity().getContentResolver().takePersistableUriPermission(data.getData(),
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
@Override
public void onDestroy() {
if (mLoadFileTask != null) {
mStopLoading = true;
mLoadFileTask.cancel(true);
mLoadFileTask = null;
}
mSearchResultsPopup.dismiss();
super.onDestroy();
}
public void changeMode() {
mAdapter.setMode(mAdapter.getMode() == LinesAdapter.MODE_VIEW ? LinesAdapter.MODE_EDIT : LinesAdapter.MODE_VIEW);
updateAdapter();
}
public int getMode() {
return mAdapter.getMode();
}
public void setHandler(Handler handler) {
mHandler = handler;
}
public void setEncoding(Charset charset) {
mSelectedCharset = charset;
openSelectedFile();
}
public void gotoLine(final int lineNumber, int type) {
int totalLinesNumber = mAdapter.getCount();
int position = type == EditViewGotoDialog.GOTO_LINE_POSITION ?
lineNumber : (int) (totalLinesNumber / 100.0 * lineNumber);
if (position > totalLinesNumber) {
position = totalLinesNumber;
}
mList.setSelection(position);
}
public void openFile(final File file) {
mFile = file;
String charset = App.sInstance.getSettings().getDefaultCharset();
if (charset == null) {
setEncoding(Charset.defaultCharset());
showSelectEncodingDialog();
} else {
setEncoding(Charset.forName(charset));
}
}
public boolean isFileTooBig() {
return mFile.length() > MAX_FILE_SIZE;
}
private void openSelectedFile() {
mProgress.setVisibility(View.VISIBLE);
if (isFileTooBig()) {
mBigFile = true;
mHandler.sendMessage(Message.obtain(mHandler, MSG_BIG_FILE));
Log.d(TAG, "file to large to be opened in edit mode");
ToastNotification.makeText(Viewer.this.getActivity(),
App.sInstance.getString(R.string.error_file_is_too_big), Toast.LENGTH_SHORT).show();
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
if (i == mAdapter.getCount() - 1 && mBigText.hasBottomFragment()) {
loadBifFileFragment(mBigText.nextFragment());
} else if (i == 0 && mBigText.hasUpperFragment()) {
loadBifFileFragment(mBigText.previousFragment());
}
}
});
}
mAdapter = new LinesAdapter(mBigFile ? mBigText : mText);
mList.setAdapter(mAdapter);
mStopLoading = false;
mLoadFileTask = new LoadFileTask();
//noinspection unchecked
mLoadFileTask.execute();
}
private void loadBifFileFragment(int fragmentNumber) {
if (mLoadTextFragmentTask != null) {
mLoadTextFragmentTask.cancel(true);
}
mLoadTextFragmentTask = new LoadTextFragmentTask(fragmentNumber);
//noinspection unchecked
mLoadTextFragmentTask.execute();
}
public void search(String pattern, boolean caseSensitive, boolean wholeWords, boolean regularExpression) {
mAdapter.search(pattern, caseSensitive, wholeWords, regularExpression);
doSearch(pattern, caseSensitive, wholeWords, regularExpression);
}
public void doSearch(final String pattern, final boolean caseSensitive,
final boolean wholeWords, final boolean regularExpression) {
if (!mSearchResultsPopup.isShowing()) {
mSearchResultsPopup.show();
}
final List<Integer> searchLines = Collections.synchronizedList(new ArrayList<Integer>());
final Map<Integer, int[]> wordsOnLine = Collections.synchronizedMap(new HashMap<Integer, int[]>());
View view = mSearchResultsPopup.getContentView();
final View progress = view.findViewById(R.id.search_progress);
final TextView matches = (TextView) view.findViewById(R.id.search_found);
final View next = view.findViewById(R.id.search_next);
final View prev = view.findViewById(R.id.search_prev);
final View close = view.findViewById(R.id.search_close);
progress.setVisibility(View.VISIBLE);
prev.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = mList.getFirstVisiblePosition();
for (Integer pos : new ReversedIterator<>(searchLines)) {
TextView textView = (TextView) getChildView(pos);
if (pos < position) {
gotoLine(pos - 1, EditViewGotoDialog.GOTO_LINE_POSITION);
return;
} else if (textView != null && textView.getLineCount() > 1 && wordsOnLine.get(pos).length > 1) {
Layout layout = textView.getLayout();
calculateRowHeight(textView, layout);
int firstVisibleRow = calculateFirstVisibleRow(textView);
if (layout.getLineCount() < firstVisibleRow) {
continue;
}
int firstSearchIndex = layout.getLineEnd(firstVisibleRow - 1);
int prevPosition = wordsOnLine.get(pos)[0];
for (int wordPosition : wordsOnLine.get(pos)) {
if (wordPosition > firstSearchIndex) {
for (int j = firstVisibleRow; j >= 0; j--) {
if (isSearchWordOnLine(layout, prevPosition, j)) {
// no need to scroll to the same line - let's find next occurrence
if (firstVisibleRow == j) {
break;
}
scrollToLine(textView, firstVisibleRow, j);
return;
}
}
} else {
prevPosition = wordPosition;
}
}
}
}
}
});
next.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = mList.getFirstVisiblePosition() + 2;
for (Integer pos : searchLines) {
TextView textView = (TextView) getChildView(pos);
if (pos > position) {
gotoLine(pos - 1, EditViewGotoDialog.GOTO_LINE_POSITION);
return;
} else if (textView != null && textView.getLineCount() > 1 && wordsOnLine.get(pos).length > 1) {
Layout layout = textView.getLayout();
calculateRowHeight(textView, layout);
int firstVisibleRow = calculateFirstVisibleRow(textView);
int firstSearchIndex = layout.getLineEnd(firstVisibleRow);
for (int wordPosition : wordsOnLine.get(pos)) {
if (wordPosition > firstSearchIndex) {
for (int j = firstVisibleRow + 1; j < layout.getLineCount(); j++) {
if (isSearchWordOnLine(layout, wordPosition, j)) {
// no need to scroll to the same line - let's find next occurrence
if (firstVisibleRow == j) {
break;
}
scrollToLine(textView, firstVisibleRow, j);
return;
}
}
}
}
}
}
}
});
matches.setText("0");
final Subscription subscription = Observable.create(new Observable.OnSubscribe<SearchResult>() {
@Override
public void call(Subscriber<? super SearchResult> subscriber) {
ArrayList<String> lines = mAdapter.getText();
SearchResult searchResult = new SearchResult();
for (int i = 0; i < lines.size(); i++) {
searchResult.reset();
String line = lines.get(i);
doSearchInText(line, pattern, caseSensitive, wholeWords, regularExpression, searchResult);
if (searchResult.count > 0) {
searchResult.lineNumber = i;
subscriber.onNext(searchResult);
}
}
subscriber.onCompleted();
}
}).subscribeOn(Schedulers.from(getThreadPool())).subscribe(new Subscriber<SearchResult>() {
private int mTotalOccurrence;
@Override
public void onCompleted() {
updateUi(new Runnable() {
@Override
public void run() {
progress.setVisibility(View.GONE);
}
});
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(SearchResult searchResult) {
//System.out.println(":::: " + searchResult.first + " " + searchResult.second);
mTotalOccurrence += searchResult.count;
searchLines.add(searchResult.lineNumber);
int[] wordPositions = new int[searchResult.positions.size()];
int i = 0;
for (Integer position : searchResult.positions) {
wordPositions[i++] = position;
}
wordsOnLine.put(searchResult.lineNumber, wordPositions);
updateUi(mUpdateOccurrences);
}
private Runnable mUpdateOccurrences = new Runnable() {
@Override
public void run() {
matches.setText(String.valueOf(mTotalOccurrence));
}
};
});
close.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
subscription.unsubscribe();
mAdapter.stopSearch();
mSearchResultsPopup.dismiss();
}
});
}
private int calculateFirstVisibleRow(TextView textView) {
return Math.abs(Math.round(textView.getY() / mCalculatedRowHeight));
}
private boolean isSearchWordOnLine(Layout layout, int wordPosition, int j) {
return wordPosition >= layout.getLineStart(j) && wordPosition <= layout.getLineEnd(j);
}
private void scrollToLine(TextView textView, int firstVisibleRow, int requiredRow) {
int scrollBy = mCalculatedRowHeight * (requiredRow - firstVisibleRow);
mList.smoothScrollBy(scrollBy + (int) (textView.getY() + firstVisibleRow * mCalculatedRowHeight), 0);
}
private void calculateRowHeight(TextView textView, Layout layout) {
if (mCalculatedRowHeight == -1) {
mCalculatedRowHeight = layout.getHeight() / textView.getLineCount();
}
}
private View getChildView(int pos) {
int firstPosition = mList.getFirstVisiblePosition() - mList.getHeaderViewsCount();
int wantedChild = pos - firstPosition;
return mList.getChildAt(wantedChild);
}
private void doSearchInText(String string, String pattern, boolean caseSensitive, boolean wholeWords, boolean regularExpression, SearchResult result) {
Pattern patternMatch = FileUtilsExt.createWordSearchPattern(pattern, wholeWords, caseSensitive ? IOCase.SENSITIVE : IOCase.INSENSITIVE);
Matcher matcher = patternMatch.matcher(string);
while (matcher.find()) {
result.positions.add(matcher.start());
result.count++;
}
}
private class SearchResult {
int lineNumber;
int count;
LinkedList<Integer> positions = new LinkedList<>();
public void reset() {
count = 0;
positions.clear();
}
}
public void save() {
mAdapter.saveCurrentEditLine(getActivity().getCurrentFocus());
mSaveObservable.subscribe(new Subscriber<Boolean>() {
@Override
public void onCompleted() {
mHandler.sendMessage(Message.obtain(mHandler, MSG_TEXT_CHANGED, mText.isTextChanged()));
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
if (e instanceof SdcardPermissionException && Build.VERSION.SDK_INT >= 21) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, BaseFileSystemPanel.REQUEST_CODE_REQUEST_PERMISSION);
}
}
@Override
public void onNext(Boolean aBoolean) {
}
});
}
public void replace(final String pattern, final String replaceTo, final boolean caseSensitive,
final boolean wholeWords, final boolean regularExpression) {
Observable.create(new Observable.OnSubscribe<Boolean>() {
@Override
public void call(Subscriber<? super Boolean> subscriber) {
mText.replace(pattern, replaceTo, caseSensitive ? IOCase.SENSITIVE : IOCase.INSENSITIVE,
wholeWords, regularExpression);
subscriber.onCompleted();
}
}).subscribeOn(Schedulers.from(getThreadPool())).subscribe(new Subscriber<Boolean>() {
@Override
public void onCompleted() {
updateUi(onReplaceCompleted);
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onNext(Boolean aBoolean) {
}
private Runnable onReplaceCompleted = new Runnable() {
@Override
public void run() {
updateAdapter();
mSearchResultsPopup.dismiss();
mHandler.sendMessage(Message.obtain(mHandler, MSG_TEXT_CHANGED, mText.isTextChanged()));
}
};
});
}
// TODO: we need to avoid this говнокод!!!
private class LoadTextFragmentTask extends AsyncTask<Void, Void, ArrayList<String>> {
private int mFragment;
private int mStartPosition;
private LoadTextFragmentTask(int fragment) {
mFragment = fragment;
mStartPosition = fragment * LINES_COUNT_FRAGMENT;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
mProgress.setVisibility(View.VISIBLE);
}
@Override
protected ArrayList<String> doInBackground(Void... voids) {
BufferedReader is = null;
ArrayList<String> lines = new ArrayList<String>();
try {
is = new BufferedReader(new InputStreamReader(new FileInputStream(mFile), mSelectedCharset.name()));
String line;
int count = 0;
while (count < mStartPosition && (is.readLine()) != null) {
count++;
}
count = 0;
while (count < LINES_COUNT_FRAGMENT && (line = is.readLine()) != null) {
lines.add(line);
count++;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return lines;
}
@Override
protected void onPostExecute(ArrayList<String> result) {
super.onPostExecute(result);
mBigText.setLines(result);
mProgress.setVisibility(View.GONE);
mList.post(new Runnable() {
public void run() {
mList.setSelection(0);
mList.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
mAdapter.notifyDataSetChanged();
mList.getViewTreeObserver().removeOnScrollChangedListener(this);
}
});
}
});
}
}
private class LoadFileTask extends AsyncTask<Void, Void, Void> {
private ArrayList<String> mLines;
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Void doInBackground(Void... voids) {
mLines = new ArrayList<String>();
if (mBigFile) {
loadFragmentsFileStrings();
} else {
loadFileStrings();
}
return null;
}
private void loadFragmentsFileStrings() {
BufferedReader is = null;
try {
is = new BufferedReader(new InputStreamReader(new FileInputStream(mFile), mSelectedCharset.name()));
String line;
int count = 0;
while (!mStopLoading && count < LINES_COUNT_FRAGMENT && (line = is.readLine()) != null) {
mLines.add(line);
count++;
}
} catch (Exception e) {
e.printStackTrace();
} catch (OutOfMemoryError e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void loadFileStrings() {
BufferedReader is = null;
try {
if(mFile.canRead()){
is = new BufferedReader(new InputStreamReader(new FileInputStream(mFile), mSelectedCharset.name()));
}else{
is = new BufferedReader(RootTask.readFile(mFile));
}
if (App.sInstance.getSettings().isReplaceDelimeters()) {
loadLinesReplaceSpecialCh(is);
} else {
loadLines(is);
}
} catch (IOException e) {
e.printStackTrace();
} catch (OutOfMemoryError e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void loadLines(BufferedReader is) throws IOException {
String line;
while (!mStopLoading && (line = is.readLine()) != null) {
mLines.add(line);
}
}
private void loadLinesReplaceSpecialCh(BufferedReader is) throws IOException {
String line;
while (!mStopLoading && (line = is.readLine()) != null) {
for (int i = 1; i <= 31; i++) {
line = line.replace(Character.toString((char) i), "0x" + Integer.toString(i, 16));
}
mLines.add(line);
}
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
mProgress.setVisibility(View.GONE);
mAdapter.swapData(mLines);
}
}
private void updateAdapter() {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mAdapter.notifyDataSetChanged();
}
});
}
public void showSelectEncodingDialog() {
mCharsetSelectDialog = new SelectEncodingDialog(getActivity(), mHandler, mFile);
mCharsetSelectDialog.show();
mCharsetSelectDialog.setCancelable(true);
adjustDialogSize(mCharsetSelectDialog);
}
/**
* Adjust dialog size. Actuall for old android version only (due to absence of Holo themes).
*
* @param dialog dialog whose size should be adjusted.
*/
private void adjustDialogSize(Dialog dialog) {
DisplayMetrics metrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.copyFrom(dialog.getWindow().getAttributes());
params.width = (int) (metrics.widthPixels * 0.65);
params.height = (int) (metrics.heightPixels * 0.55);
dialog.getWindow().setAttributes(params);
}
private void updateUi(Runnable runnable) {
getActivity().runOnUiThread(runnable);
}
private Observable<Boolean> mSaveObservable = Observable.create(new Observable.OnSubscribe<Boolean>() {
@Override
public void call(Subscriber<? super Boolean> subscriber) {
try {
String sdCardPath = SystemUtils.getExternalStorage(mFile.getAbsolutePath());
if (checkUseStorageApi(sdCardPath)) {
checkForPermissionAndGetBaseUri();
OutputStream stream = StorageUtils.getStorageOutputFileStream(mFile, sdCardPath);
mText.save(stream);
} else {
mText.save(mFile);
}
} catch (Exception e) {
subscriber.onError(e);
}
subscriber.onCompleted();
}
}).subscribeOn(Schedulers.from(getThreadPool()));
}