Began implementing RxJava

This commit is contained in:
William Brawner 2017-08-11 18:09:11 -05:00
parent f5a7333e84
commit e4ba6d39bc
5 changed files with 150 additions and 392 deletions

View file

@ -1,5 +1,7 @@
package com.wbrawner.simplemarkdown; package com.wbrawner.simplemarkdown;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -44,159 +46,29 @@ import static android.content.ContentValues.TAG;
public class EditFragment extends Fragment { public class EditFragment extends Fragment {
public static final String SAVE_ACTION = "com.wbrawner.simplemarkdown.ACTION_SAVE"; public static final String SAVE_ACTION = "com.wbrawner.simplemarkdown.ACTION_SAVE";
public static final String LOAD_ACTION = "com.wbrawner.simplemarkdown.ACTION_LOAD"; public static final String LOAD_ACTION = "com.wbrawner.simplemarkdown.ACTION_LOAD";
private static EditText mMarkdownEditor; private MarkdownViewModel markdownViewModel;
private FileUtils mFileUtils;
@BindView(R.id.markdown_edit) @BindView(R.id.markdown_edit)
EditText markdownEditor; EditText markdownEditor;
private FragmentActivity mContext;
private File mTmpFile;
private boolean loadTmpFile = true;
public EditFragment() { public EditFragment() {
// Required empty public constructor // Required empty public constructor
} }
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
IntentFilter filter = new IntentFilter();
filter.addAction(SAVE_ACTION);
filter.addAction(LOAD_ACTION);
LocalBroadcastManager.getInstance(getContext()).registerReceiver(
new EditFragment.MarkdownBroadcastSaveReceiver(),
filter
);
mContext = getActivity();
mFileUtils = new FileUtils(mContext);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
// Inflate the layout for this fragment // Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_edit, container, false); View view = inflater.inflate(R.layout.fragment_edit, container, false);
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
mMarkdownEditor = markdownEditor; markdownViewModel = ViewModelProviders.of(getActivity()).get(MarkdownViewModel.class);
if (mMarkdownEditor.requestFocus()) { Observable<String> obs = RxTextView.textChanges(markdownEditor)
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
}
Observable<String> obs = RxTextView.textChanges(mMarkdownEditor)
.debounce(50, TimeUnit.MILLISECONDS).map(editable -> editable.toString()); .debounce(50, TimeUnit.MILLISECONDS).map(editable -> editable.toString());
obs.subscribeOn(Schedulers.io()); obs.subscribeOn(Schedulers.io());
obs.observeOn(AndroidSchedulers.mainThread()); obs.observeOn(AndroidSchedulers.mainThread());
obs.subscribe(string -> { obs.subscribe(data -> {
Log.d(TAG, "debounced " + string); markdownViewModel.updateMarkdown(data);
updatePreview(mContext);
}); });
return view; return view;
} }
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (getActivity().getIntent().getAction().equals(Intent.ACTION_MAIN)) {
File tmpFile = new File(getActivity().getFilesDir() + "/" + MainActivity.getTempFileName());
if (tmpFile.exists()) {
FileLoadTask loadTask = new FileLoadTask(mContext, EditFragment.this);
loadTask.execute(FileProvider.getUriForFile(mContext, MainActivity.AUTHORITY, tmpFile));
}
}
}
public static void updatePreview(Context context) {
Intent broadcastIntent = new Intent(PreviewFragment.PREVIEW_ACTION);
broadcastIntent.putExtra("markdownData", mMarkdownEditor.getText().toString());
Layout layout = mMarkdownEditor.getLayout();
if (layout != null) {
int line =
layout.getLineForOffset(mMarkdownEditor.getSelectionStart());
int baseline = layout.getLineBaseline(line);
int ascent = layout.getLineAscent(line);
float yPos = (baseline + ascent) * 1.0f;
float yPercent = yPos / mMarkdownEditor.getMeasuredHeight();
broadcastIntent.putExtra("scrollY", yPercent);
}
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context);
manager.sendBroadcast(broadcastIntent);
}
public void save(String data, String filePath) {
// TODO: move this to AsyncTask
if (!mFileUtils.isExternalStorageWriteable()) {
mFileUtils.requestWritePermissions();
return;
}
if (filePath == null) {
filePath = MainActivity.getFilePath() + MainActivity.getFileName();
}
FileOutputStream out = null;
try {
Log.d(TAG, "File path: " + filePath);
File tmpFile = new File(filePath);
out = new FileOutputStream(tmpFile);
out.write(data.getBytes());
} catch (Exception e) {
Log.e(TAG, "Error saving temp file:", e);
} finally {
try {
if (out != null) {
out.close();
Toast.makeText(mContext, getString(R.string.file_saved, filePath), Toast.LENGTH_SHORT)
.show();
}
} catch (IOException e) {
Log.e(TAG, "Error closing write stream", e);
}
}
}
public void save(String data) {
save(data, null);
}
public void save() {
save(mMarkdownEditor.getText().toString(), null);
}
@Override
public void onPause() {
save(mMarkdownEditor.getText().toString(),
MainActivity.getTempFilePath() + MainActivity.getFileName());
super.onPause();
}
public void setEditorText(String s) {
mMarkdownEditor.setText(s);
}
private class MarkdownBroadcastSaveReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Intent received: " + intent.getAction());
switch (intent.getAction()) {
case SAVE_ACTION:
if (intent.hasExtra("fileName")) {
String fileName = intent.getStringExtra("fileName");
if (!fileName.contains("/")) {
fileName = MainActivity.getFilePath() + "/" + fileName;
}
if (!fileName.endsWith(".md"))
fileName += ".md";
save(mMarkdownEditor.getText().toString(), fileName);
}
break;
case LOAD_ACTION:
if (intent.hasExtra("fileUri")) {
FileLoadTask loadTask = new FileLoadTask(mContext, EditFragment.this);
loadTask.execute(Uri.parse(intent.getStringExtra("fileUri")));
}
break;
}
}
}
} }

View file

@ -1,76 +0,0 @@
package com.wbrawner.simplemarkdown;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.EditText;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import static android.content.ContentValues.TAG;
/**
* Created by billy on 7/25/17.
*/
public class FileLoadTask extends AsyncTask<Uri, String, String> {
private Context mContext;
private EditFragment mEditFragment;
public FileLoadTask(Context context, EditFragment editFragment) {
mContext = context;
mEditFragment = editFragment;
}
@Override
protected String doInBackground(Uri... uris) {
if (mContext == null) {
return null;
}
BufferedReader reader = null;
StringBuilder sb = new StringBuilder();
FileOutputStream out = null;
try {
InputStream in = mContext.getContentResolver().openInputStream(uris[0]);
File tmpFile = new File(mContext.getFilesDir() + "/" + MainActivity.getTempFileName());
if (tmpFile.exists())
tmpFile.delete();
out = new FileOutputStream(tmpFile);
reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
sb.append("\r\n");
out.write(line.getBytes());
out.write("\r\n".getBytes());
}
} catch (Exception e) {
Log.e(TAG, "Error opening file:", e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception e) {
}
}
if (out != null) {
try {
out.close();
} catch (Exception e) {}
}
}
return sb.toString();
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
mEditFragment.setEditorText(s);
}
}

View file

@ -60,32 +60,6 @@ public class MainActivity extends AppCompatActivity
TabLayout tabLayout; TabLayout tabLayout;
private static final String TAG = MainActivity.class.getSimpleName(); private static final String TAG = MainActivity.class.getSimpleName();
private static String fileName;
public static String getTempFileName() {
return "tmp_" + getFileName();
}
public static String getFileName() {
if (fileName == null) {
return "untitled.md";
}
if (!fileName.endsWith(".md"))
return fileName + ".md";
return fileName;
}
public static String getTempFilePath() {
return mFilesDir + "/tmp/";
}
public static String getFilePath() {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/";
}
public static void setFileName(String fileName) {
MainActivity.fileName = fileName;
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -127,79 +101,79 @@ public class MainActivity extends AppCompatActivity
return true; return true;
} }
@Override // @Override
public boolean onOptionsItemSelected(MenuItem item) { // public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { // switch (item.getItemId()) {
case R.id.action_save: // case R.id.action_save:
//TODO: Create popup for file name // //TODO: Create popup for file name
AlertDialog.Builder builder = new AlertDialog.Builder(this); // AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.action_save); // builder.setTitle(R.string.action_save);
//
final EditText input = new EditText(this); // final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT); // input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setHint(R.string.hint_filename); // input.setHint(R.string.hint_filename);
input.setText(getFileName()); // input.setText(getFileName());
builder.setView(input); // builder.setView(input);
//
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { // builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override // @Override
public void onClick(DialogInterface dialog, int which) { // public void onClick(DialogInterface dialog, int which) {
if (input.getText().length() > 0) { // if (input.getText().length() > 0) {
setFileName(input.getText().toString()); // setFileName(input.getText().toString());
requestSave(input.getText().toString()); // requestSave(input.getText().toString());
} // }
} // }
}); // });
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { // builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override // @Override
public void onClick(DialogInterface dialog, int which) { // public void onClick(DialogInterface dialog, int which) {
dialog.cancel(); // dialog.cancel();
} // }
}); // });
//
builder.show(); // builder.show();
break; // break;
case R.id.action_share: // case R.id.action_share:
//TODO: Fix this // //TODO: Fix this
File tmpFile = new File(getTempFilePath() + getFileName()); // File tmpFile = new File(getTempFilePath() + getFileName());
if (!tmpFile.exists()) { // if (!tmpFile.exists()) {
Intent saveIntent = new Intent(EditFragment.SAVE_ACTION); // Intent saveIntent = new Intent(EditFragment.SAVE_ACTION);
saveIntent.putExtra("fileName", getTempFilePath() + getFileName()); // saveIntent.putExtra("fileName", getTempFilePath() + getFileName());
LocalBroadcastManager.getInstance(getApplicationContext()) // LocalBroadcastManager.getInstance(getApplicationContext())
.sendBroadcast(saveIntent); // .sendBroadcast(saveIntent);
} // }
Uri fileUri = FileProvider.getUriForFile(MainActivity.this, AUTHORITY, tmpFile); // Uri fileUri = FileProvider.getUriForFile(MainActivity.this, AUTHORITY, tmpFile);
if (fileUri != null) { // if (fileUri != null) {
try { // try {
Intent shareIntent = new Intent(Intent.ACTION_SEND); // Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setDataAndType( // shareIntent.setDataAndType(
fileUri, // fileUri,
getContentResolver().getType(fileUri) // getContentResolver().getType(fileUri)
); // );
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri); // shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity( // startActivity(
Intent.createChooser( // Intent.createChooser(
shareIntent, // shareIntent,
getString(R.string.share_file) // getString(R.string.share_file)
) // )
); // );
} catch (ActivityNotFoundException e) { // } catch (ActivityNotFoundException e) {
Log.e(TAG, "Error sharing file", e); // Log.e(TAG, "Error sharing file", e);
Toast.makeText( // Toast.makeText(
MainActivity.this, // MainActivity.this,
R.string.no_shareable_apps, // R.string.no_shareable_apps,
Toast.LENGTH_SHORT // Toast.LENGTH_SHORT
).show(); // ).show();
} // }
} // }
break; // break;
case R.id.action_load: // case R.id.action_load:
requestOpen(); // requestOpen();
break; // break;
} // }
return super.onOptionsItemSelected(item); // return super.onOptionsItemSelected(item);
} // }
private void requestSave(String text) { private void requestSave(String text) {
Intent saveIntent = new Intent(EditFragment.SAVE_ACTION); Intent saveIntent = new Intent(EditFragment.SAVE_ACTION);
@ -226,25 +200,25 @@ public class MainActivity extends AppCompatActivity
} }
} }
@Override // @Override
public void onRequestPermissionsResult(int requestCode, // public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) { // String permissions[], int[] grantResults) {
switch (requestCode) { // switch (requestCode) {
case FileUtils.WRITE_PERMISSION_REQUEST: { // case FileUtils.WRITE_PERMISSION_REQUEST: {
// If request is cancelled, the result arrays are empty. // // If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 // if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { // && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, open file chooser dialog // // Permission granted, open file chooser dialog
requestSave(getFileName()); // requestSave(getFileName());
} else { // } else {
// Permission denied, do nothing // // Permission denied, do nothing
Toast.makeText(MainActivity.this, R.string.no_permissions, Toast.LENGTH_SHORT) // Toast.makeText(MainActivity.this, R.string.no_permissions, Toast.LENGTH_SHORT)
.show(); // .show();
} // }
return; // return;
} // }
} // }
} // }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (pager.getCurrentItem() == FRAGMENT_EDIT) if (pager.getCurrentItem() == FRAGMENT_EDIT)

View file

@ -0,0 +1,37 @@
package com.wbrawner.simplemarkdown;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import com.commonsware.cwac.anddown.AndDown;
public class MarkdownViewModel extends ViewModel {
private MutableLiveData<String> markdownLiveData;
private MutableLiveData<String> htmlLiveData = new MutableLiveData<>();;
public MarkdownViewModel() {
markdownLiveData = new MutableLiveData<>();
}
public void updateMarkdown(String data) {
if (markdownLiveData == null)
markdownLiveData = new MutableLiveData<>();
markdownLiveData.postValue(data);
Runnable generateMarkdown = () -> {
AndDown andDown = new AndDown();
int hoedownFlags =
AndDown.HOEDOWN_EXT_STRIKETHROUGH | AndDown.HOEDOWN_EXT_TABLES |
AndDown.HOEDOWN_EXT_UNDERLINE | AndDown.HOEDOWN_EXT_SUPERSCRIPT |
AndDown.HOEDOWN_EXT_FENCED_CODE;
htmlLiveData.postValue(andDown.markdownToHtml(markdownLiveData.getValue(), hoedownFlags, 0));
};
if (markdownLiveData.getValue() != null)
generateMarkdown.run();
}
public LiveData<String> getHtml() {
return htmlLiveData;
}
}

View file

@ -1,6 +1,9 @@
package com.wbrawner.simplemarkdown; package com.wbrawner.simplemarkdown;
import android.Manifest; import android.Manifest;
import android.arch.lifecycle.LifecycleFragment;
import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProviders;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -22,13 +25,20 @@ import android.webkit.WebView;
import com.commonsware.cwac.anddown.AndDown; import com.commonsware.cwac.anddown.AndDown;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import io.reactivex.Observable;
import io.reactivex.processors.PublishProcessor;
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
public class PreviewFragment extends Fragment { public class PreviewFragment extends LifecycleFragment {
private static final String TAG = PreviewFragment.class.getSimpleName(); private static final String TAG = PreviewFragment.class.getSimpleName();
private static final int INTERNET_REQUEST = 0; private static final int INTERNET_REQUEST = 0;
private WebView mMarkdownView; private MarkdownViewModel markdownViewModel;
@BindView(R.id.markdown_view) @BindView(R.id.markdown_view)
WebView markdownView; WebView markdownView;
@ -40,76 +50,17 @@ public class PreviewFragment extends Fragment {
// Required empty public constructor // Required empty public constructor
} }
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
IntentFilter filter = new IntentFilter();
filter.addAction(PREVIEW_ACTION);
LocalBroadcastManager.getInstance(getContext()).registerReceiver(
new MarkdownBroadcastSender(),
filter
);
checkPermissions();
}
private void checkPermissions() {
if (ContextCompat.checkSelfPermission(getActivity(),
Manifest.permission.INTERNET)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(getActivity(),
new String[]{Manifest.permission.INTERNET},
INTERNET_REQUEST);
}
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
// Inflate the layout for this fragment // Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_preview, container, false); View view = inflater.inflate(R.layout.fragment_preview, container, false);
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
mMarkdownView = markdownView; markdownViewModel = ViewModelProviders.of(getActivity()).get(MarkdownViewModel.class);
markdownViewModel.getHtml().observe(this, s -> markdownView.loadData(s, "text/html", "UTF-8"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true); WebView.setWebContentsDebuggingEnabled(true);
} }
return view; return view;
} }
private class MarkdownBroadcastSender extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Intent received: " + intent.getAction());
switch (intent.getAction()) {
case PREVIEW_ACTION:
if (intent.hasExtra("markdownData")) {
String data = intent.getStringExtra("markdownData");
int yPos = 0;
if (intent.hasExtra("scrollY")) {
float yPercent = intent.getFloatExtra("scrollY", 0);
Log.d(TAG, "Scrolling to: " + yPercent);
yPos = Math.round(mMarkdownView.getContentHeight() * yPercent);
}
markdown(data, yPos);
}
break;
}
}
}
private void markdown(final String text, final int scrollY) {
Thread setMarkdown = new Thread() {
@Override
public void run() {
AndDown andDown = new AndDown();
int hoedownFlags =
AndDown.HOEDOWN_EXT_STRIKETHROUGH | AndDown.HOEDOWN_EXT_TABLES |
AndDown.HOEDOWN_EXT_UNDERLINE | AndDown.HOEDOWN_EXT_SUPERSCRIPT |
AndDown.HOEDOWN_EXT_FENCED_CODE;
String html = andDown.markdownToHtml(text, hoedownFlags, 0);
mMarkdownView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
mMarkdownView.scrollTo(0, scrollY);
}
};
setMarkdown.run();
}
} }