package yuku.alkitab.base.ac;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.text.SpannableStringBuilder;
import android.text.style.BackgroundColorSpan;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.SearchView;
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog;
import yuku.afw.V;
import yuku.afw.storage.Preferences;
import yuku.afw.widget.EasyAdapter;
import yuku.alkitab.base.App;
import yuku.alkitab.base.IsiActivity;
import yuku.alkitab.base.S;
import yuku.alkitab.base.U;
import yuku.alkitab.base.ac.base.BaseActivity;
import yuku.alkitab.base.dialog.TypeBookmarkDialog;
import yuku.alkitab.base.dialog.TypeHighlightDialog;
import yuku.alkitab.base.storage.Db;
import yuku.alkitab.base.storage.Prefkey;
import yuku.alkitab.base.util.Appearances;
import yuku.alkitab.base.util.Debouncer;
import yuku.alkitab.base.util.Highlights;
import yuku.alkitab.base.util.QueryTokenizer;
import yuku.alkitab.base.util.SearchEngine;
import yuku.alkitab.base.util.Sqlitil;
import yuku.alkitab.base.widget.VerseRenderer;
import yuku.alkitab.debug.R;
import yuku.alkitab.model.Label;
import yuku.alkitab.model.Marker;
import yuku.alkitab.model.Version;
import yuku.alkitab.util.Ari;
import yuku.alkitabintegration.display.Launcher;
import yuku.devoxx.flowlayout.FlowLayout;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class MarkerListActivity extends BaseActivity {
public static final String TAG = MarkerListActivity.class.getSimpleName();
private static final int REQCODE_edit_note = 1;
// in
private static final String EXTRA_filter_kind = "filter_kind";
private static final String EXTRA_filter_labelId = "filter_labelId";
public static final int LABELID_noLabel = -1;
/**
* Action to broadcast when marker list needs to be reloaded due to some background changes
*/
public static final String ACTION_RELOAD = MarkerListActivity.class.getName() + ".action.RELOAD";
View root;
View empty;
TextView tEmpty;
View bClearFilter;
View progress;
SearchView searchView;
ListView lv;
View emptyView;
MarkerListAdapter adapter;
String sort_column;
boolean sort_ascending;
@IdRes int sort_columnId;
String currentlyUsedFilter;
List<Marker> allMarkers;
Marker.Kind filter_kind;
long filter_labelId;
int hiliteColor;
Version version = S.activeVersion;
String versionId = S.activeVersionId;
float textSizeMult = S.getDb().getPerVersionSettings(versionId).fontSizeMultiplier;
public static Intent createIntent(Context context, Marker.Kind filter_kind, long filter_labelId) {
Intent res = new Intent(context, MarkerListActivity.class);
res.putExtra(EXTRA_filter_kind, filter_kind.code);
res.putExtra(EXTRA_filter_labelId, filter_labelId);
return res;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_marker_list);
final Toolbar toolbar = V.get(this, R.id.toolbar);
setSupportActionBar(toolbar);
final ActionBar ab = getSupportActionBar();
assert ab != null;
ab.setDisplayHomeAsUpEnabled(true);
root = V.get(this, R.id.root);
empty = V.get(this, android.R.id.empty);
tEmpty = V.get(this, R.id.tEmpty);
bClearFilter = V.get(this, R.id.bClearFilter);
progress = V.get(this, R.id.progress);
lv = V.get(this, android.R.id.list);
emptyView = V.get(this, android.R.id.empty);
filter_kind = Marker.Kind.fromCode(getIntent().getIntExtra(EXTRA_filter_kind, 0));
filter_labelId = getIntent().getLongExtra(EXTRA_filter_labelId, 0);
bClearFilter.setOnClickListener(v -> searchView.setQuery("", true));
setTitleAndNothingText();
// default sort ...
sort_column = Db.Marker.createTime;
sort_ascending = false;
sort_columnId = R.id.menuSortCreateTime;
{ // .. but probably there is a stored preferences about the last sort used
String pref_sort_column = Preferences.getString(Prefkey.marker_list_sort_column);
if (pref_sort_column != null) {
sort_ascending = Preferences.getBoolean(Prefkey.marker_list_sort_ascending, false);
switch (pref_sort_column) {
case "waktuTambah": // add time (for compat when upgrading from prev ver)
case Db.Marker.createTime:
sort_column = Db.Marker.createTime;
sort_columnId = R.id.menuSortCreateTime;
break;
case "waktuUbah": // modify time (for compat when upgrading from prev ver)
case Db.Marker.modifyTime:
sort_column = Db.Marker.modifyTime;
sort_columnId = R.id.menuSortModifyTime;
break;
case Db.Marker.ari:
sort_column = Db.Marker.ari;
sort_columnId = R.id.menuSortAri;
break;
case "tulisan": // caption (for compat when upgrading from prev ver)
case Db.Marker.caption:
sort_column = Db.Marker.caption;
sort_columnId = R.id.menuSortCaption;
break;
default:
// do nothing!
}
}
}
adapter = new MarkerListAdapter();
lv.setAdapter(adapter);
lv.setCacheColorHint(S.applied.backgroundColor);
lv.setOnItemClickListener(lv_itemClick);
lv.setOnItemLongClickListener(lv_itemLongClick);
lv.setEmptyView(emptyView);
App.getLbm().registerReceiver(br, new IntentFilter(ACTION_RELOAD));
}
@Override
protected void onDestroy() {
super.onDestroy();
App.getLbm().unregisterReceiver(br);
}
BroadcastReceiver br = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
if (ACTION_RELOAD.equals(intent.getAction())) {
loadAndFilter();
}
}
};
@Override
protected void onStart() {
super.onStart();
{ // apply background color, and clear window background to prevent overdraw
getWindow().setBackgroundDrawableResource(android.R.color.transparent);
root.setBackgroundColor(S.applied.backgroundColor);
}
tEmpty.setTextColor(S.applied.fontColor);
hiliteColor = U.getSearchKeywordTextColorByBrightness(S.applied.backgroundBrightness);
loadAndFilter();
}
void loadAndFilter() {
loadAndFilter(false);
}
void loadAndFilter(final boolean immediate) {
allMarkers = S.getDb().listMarkers(filter_kind, filter_labelId, sort_column, sort_ascending);
if (immediate) {
filter.submit(currentlyUsedFilter, 0);
} else {
filter.submit(currentlyUsedFilter);
}
}
void setTitleAndNothingText() {
String title = null;
String nothingText = null;
// set title based on filter
if (filter_kind == Marker.Kind.note) {
title = getString(R.string.bmcat_notes);
nothingText = getString(R.string.bl_no_notes_written_yet);
} else if (filter_kind == Marker.Kind.highlight) {
title = getString(R.string.bmcat_highlights);
nothingText = getString(R.string.bl_no_highlighted_verses);
} else if (filter_kind == Marker.Kind.bookmark) {
if (filter_labelId == 0) {
title = getString(R.string.bmcat_all_bookmarks);
nothingText = getString(R.string.belum_ada_pembatas_buku);
} else if (filter_labelId == LABELID_noLabel) {
title = getString(R.string.bmcat_unlabeled_bookmarks);
nothingText = getString(R.string.bl_there_are_no_bookmarks_without_any_labels);
} else {
Label label = S.getDb().getLabelById(filter_labelId);
if (label != null) {
title = label.title;
nothingText = getString(R.string.bl_there_are_no_bookmarks_with_the_label_label, label.title);
}
}
}
// if we're using text filter (as opposed to kind filter), we use a different nothingText
if (currentlyUsedFilter != null) {
nothingText = getString(R.string.bl_no_items_match_the_filter_above);
bClearFilter.setVisibility(View.VISIBLE);
} else {
bClearFilter.setVisibility(View.GONE);
}
if (title != null) {
setTitle(title);
tEmpty.setText(nothingText);
} else {
finish(); // shouldn't happen
}
}
static class FilterResult {
String query;
boolean needFilter;
List<Marker> filteredMarkers;
SearchEngine.ReadyTokens rt;
}
final Debouncer<String, FilterResult> filter = new Debouncer<String, FilterResult>(200) {
@Override
public FilterResult process(@Nullable final String payload) {
final boolean needFilter;
final String query = payload == null? "": payload.trim();
if (query.length() == 0) {
needFilter = false;
} else {
final String[] tokens = QueryTokenizer.tokenize(query);
if (tokens.length == 0) {
needFilter = false;
} else {
needFilter = true;
}
}
final String[] tokens;
if (query.length() == 0) {
tokens = null;
} else {
tokens = QueryTokenizer.tokenize(query);
}
final SearchEngine.ReadyTokens rt = tokens == null || tokens.length == 0 ? null : new SearchEngine.ReadyTokens(tokens);
final List<Marker> filteredMarkers = filterEngine(version, allMarkers, filter_kind, rt);
final FilterResult res = new FilterResult();
res.query = query;
res.needFilter = needFilter;
res.filteredMarkers = filteredMarkers;
res.rt = rt;
return res;
}
@Override
public void onResult(final FilterResult result) {
if (result.needFilter) {
currentlyUsedFilter = result.query;
} else {
currentlyUsedFilter = null;
}
setTitleAndNothingText();
adapter.setData(result.filteredMarkers, result.rt);
}
};
public static View getLabelView(LayoutInflater inflater, FlowLayout panelLabels, Label label) {
final View res = inflater.inflate(R.layout.label, panelLabels, false);
res.setLayoutParams(panelLabels.generateDefaultLayoutParams());
final TextView lCaption = V.get(res, R.id.lCaption);
lCaption.setText(label.title);
U.applyLabelColor(label, lCaption);
return res;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_marker_list, menu);
final MenuItem menuSearch = menu.findItem(R.id.menuSearch);
searchView = (SearchView) menuSearch.getActionView();
searchView.setQueryHint(getString(R.string.bl_filter_by_some_keywords));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextChange(String newText) {
filter.submit(newText);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
filter.submit(query);
return true;
}
});
return true;
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
if (requestCode == REQCODE_edit_note && resultCode == RESULT_OK) {
loadAndFilter();
App.getLbm().sendBroadcast(new Intent(IsiActivity.ACTION_ATTRIBUTE_MAP_CHANGED));
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onPrepareOptionsMenu(final Menu menu) {
final MenuItem menuSortCaption = menu.findItem(R.id.menuSortCaption);
if (filter_kind == Marker.Kind.bookmark) {
menuSortCaption.setVisible(true);
menuSortCaption.setTitle(R.string.menuSortCaption);
} else if (filter_kind == Marker.Kind.highlight) {
menuSortCaption.setVisible(true);
menuSortCaption.setTitle(R.string.menuSortCaption_color);
} else {
menuSortCaption.setVisible(false);
}
checkSortMenuItem(menu, sort_columnId, R.id.menuSortAri);
checkSortMenuItem(menu, sort_columnId, R.id.menuSortCaption);
checkSortMenuItem(menu, sort_columnId, R.id.menuSortCreateTime);
checkSortMenuItem(menu, sort_columnId, R.id.menuSortModifyTime);
return true;
}
private void checkSortMenuItem(final Menu menu, final int checkThis, final int whenThis) {
if (checkThis == whenThis) {
final MenuItem item = menu.findItem(whenThis);
if (item != null) {
item.setChecked(true);
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
switch (itemId) {
case R.id.menuSortAri:
sort(Db.Marker.ari, true, itemId);
return true;
case R.id.menuSortCaption:
sort(Db.Marker.caption, true, itemId);
return true;
case R.id.menuSortCreateTime:
sort(Db.Marker.createTime, false, itemId);
return true;
case R.id.menuSortModifyTime:
sort(Db.Marker.modifyTime, false, itemId);
return true;
}
return super.onOptionsItemSelected(item);
}
void sort(String column, boolean ascending, int columnId) {
// store for next time use
Preferences.setString(Prefkey.marker_list_sort_column, column);
Preferences.setBoolean(Prefkey.marker_list_sort_ascending, ascending);
searchView.setQuery("", true);
currentlyUsedFilter = null;
setTitleAndNothingText();
sort_column = column;
sort_ascending = ascending;
sort_columnId = columnId;
loadAndFilter();
supportInvalidateOptionsMenu();
}
final AdapterView.OnItemClickListener lv_itemClick = (parent, view, position, id) -> {
Marker marker = adapter.getItem(position);
startActivity(Launcher.openAppAtBibleLocationWithVerseSelected(marker.ari, marker.verseCount));
};
final AdapterView.OnItemLongClickListener lv_itemLongClick = (parent, view, position, id) -> {
// set menu item titles based on the kind of marker
String deleteMarker;
String editMarker;
switch (filter_kind) {
case bookmark:
deleteMarker = getString(R.string.hapus_pembatas_buku);
editMarker = getString(R.string.edit_bookmark);
break;
case note:
deleteMarker = getString(R.string.hapus_catatan);
editMarker = getString(R.string.edit_note);
break;
case highlight:
deleteMarker = getString(R.string.hapus_stabilo);
editMarker = getString(R.string.edit_highlight);
break;
default:
throw new RuntimeException("Unknown kind: " + filter_kind);
}
new MaterialDialog.Builder(this)
.items(deleteMarker, editMarker)
.itemsCallback((dialog, itemView, which, text) -> {
final Marker marker = adapter.getItem(position);
if (which == 0) {
// whatever the kind is, the way to delete is the same
S.getDb().deleteMarkerById(marker._id);
loadAndFilter();
App.getLbm().sendBroadcast(new Intent(IsiActivity.ACTION_ATTRIBUTE_MAP_CHANGED));
} else if (which == 1) {
if (filter_kind == Marker.Kind.bookmark) {
TypeBookmarkDialog dialog1 = TypeBookmarkDialog.EditExisting(this, marker._id);
dialog1.setListener(() -> {
loadAndFilter();
App.getLbm().sendBroadcast(new Intent(IsiActivity.ACTION_ATTRIBUTE_MAP_CHANGED));
});
dialog1.show();
} else if (filter_kind == Marker.Kind.note) {
startActivityForResult(NoteActivity.createEditExistingIntent(marker._id), REQCODE_edit_note);
} else if (filter_kind == Marker.Kind.highlight) {
final int ari = marker.ari;
final Highlights.Info info = Highlights.decode(marker.caption);
final String reference = version.referenceWithVerseCount(ari, marker.verseCount);
final String rawVerseText = version.loadVerseText(ari);
final VerseRenderer.FormattedTextResult ftr = new VerseRenderer.FormattedTextResult();
if (rawVerseText != null) {
VerseRenderer.render(null, null, ari, rawVerseText, "" + Ari.toVerse(ari), null, false, null, ftr);
} else {
ftr.result = ""; // verse not available
}
new TypeHighlightDialog(this, ari, newColorRgb -> {
loadAndFilter();
App.getLbm().sendBroadcast(new Intent(IsiActivity.ACTION_ATTRIBUTE_MAP_CHANGED));
}, info.colorRgb, info, reference, ftr.result);
}
}
})
.show();
return true;
};
/**
* The real work of filtering happens here.
* @param rt Tokens have to be already lowercased.
*/
public static List<Marker> filterEngine(Version version, List<Marker> allMarkers, Marker.Kind filter_kind, @Nullable SearchEngine.ReadyTokens rt) {
final List<Marker> res = new ArrayList<>();
if (rt == null) {
res.addAll(allMarkers);
return res;
}
for (final Marker marker : allMarkers) {
if (filter_kind != Marker.Kind.highlight) { // "caption" in highlights only stores color information, so it's useless to check
String caption_lc = marker.caption.toLowerCase(Locale.getDefault());
if (SearchEngine.satisfiesTokens(caption_lc, rt)) {
res.add(marker);
continue;
}
}
// try the verse text!
String verseText = version.loadVerseText(marker.ari);
if (verseText != null) { // this can be null! so beware.
String verseText_lc = verseText.toLowerCase(Locale.getDefault());
if (SearchEngine.satisfiesTokens(verseText_lc, rt)) {
res.add(marker);
}
}
}
return res;
}
class MarkerListAdapter extends EasyAdapter {
List<Marker> filteredMarkers = new ArrayList<>();
SearchEngine.ReadyTokens rt;
@Override
public Marker getItem(final int position) {
return filteredMarkers.get(position);
}
@Override
public View newView(final int position, final ViewGroup parent) {
return getLayoutInflater().inflate(R.layout.item_marker, parent, false);
}
@Override
public void bindView(final View view, final int position, final ViewGroup parent) {
final TextView lDate = V.get(view, R.id.lDate);
final TextView lCaption = V.get(view, R.id.lCaption);
final TextView lSnippet = V.get(view, R.id.lSnippet);
final FlowLayout panelLabels = V.get(view, R.id.panelLabels);
final Marker marker = getItem(position);
{
final Date createTime = marker.createTime;
final Date modifyTime = marker.modifyTime;
final String createTimeDisplay = Sqlitil.toLocaleDateMedium(createTime);
if (createTime.equals(modifyTime)) {
lDate.setText(createTimeDisplay);
} else {
final String modifyTimeDisplay = Sqlitil.toLocaleDateMedium(modifyTime);
if (U.equals(createTimeDisplay, modifyTimeDisplay)) {
// show time for modifyTime when createTime and modifyTime is on the same day
lDate.setText(getString(R.string.create_edited_modified_time, createTimeDisplay, Sqlitil.toLocaleTime(modifyTime)));
} else {
lDate.setText(getString(R.string.create_edited_modified_time, createTimeDisplay, modifyTimeDisplay));
}
}
Appearances.applyMarkerDateTextAppearance(lDate, textSizeMult);
}
final int ari = marker.ari;
final String reference = version.referenceWithVerseCount(ari, marker.verseCount);
final String caption = marker.caption;
final String rawVerseText = version.loadVerseText(ari);
final CharSequence verseText;
if (rawVerseText == null) {
verseText = getString(R.string.generic_verse_not_available_in_this_version);
} else {
final VerseRenderer.FormattedTextResult ftr = new VerseRenderer.FormattedTextResult();
VerseRenderer.render(null, null, ari, rawVerseText, "" + Ari.toVerse(ari), null, false, null, ftr);
verseText = ftr.result;
}
if (filter_kind == Marker.Kind.bookmark) {
lCaption.setText(currentlyUsedFilter != null ? SearchEngine.hilite(caption, rt, hiliteColor) : caption);
Appearances.applyMarkerTitleTextAppearance(lCaption, textSizeMult);
CharSequence snippet = currentlyUsedFilter != null ? SearchEngine.hilite(verseText, rt, hiliteColor) : verseText;
Appearances.applyMarkerSnippetContentAndAppearance(lSnippet, reference, snippet, textSizeMult);
final List<Label> labels = S.getDb().listLabelsByMarker(marker);
if (labels.size() != 0) {
panelLabels.setVisibility(View.VISIBLE);
panelLabels.removeAllViews();
for (Label label : labels) {
panelLabels.addView(getLabelView(getLayoutInflater(), panelLabels, label));
}
} else {
panelLabels.setVisibility(View.GONE);
}
} else if (filter_kind == Marker.Kind.note) {
lCaption.setText(reference);
Appearances.applyMarkerTitleTextAppearance(lCaption, textSizeMult);
lSnippet.setText(currentlyUsedFilter != null ? SearchEngine.hilite(caption, rt, hiliteColor) : caption);
Appearances.applyTextAppearance(lSnippet, textSizeMult);
} else if (filter_kind == Marker.Kind.highlight) {
lCaption.setText(reference);
Appearances.applyMarkerTitleTextAppearance(lCaption, textSizeMult);
final SpannableStringBuilder snippet = currentlyUsedFilter != null ? SearchEngine.hilite(verseText, rt, hiliteColor) : new SpannableStringBuilder(verseText);
final Highlights.Info info = Highlights.decode(caption);
if (info != null) {
final BackgroundColorSpan span = new BackgroundColorSpan(Highlights.alphaMix(info.colorRgb));
if (info.shouldRenderAsPartialForVerseText(verseText)) {
snippet.setSpan(span, info.partial.startOffset, info.partial.endOffset, 0);
} else {
snippet.setSpan(span, 0, snippet.length(), 0);
}
}
lSnippet.setText(snippet);
Appearances.applyTextAppearance(lSnippet, textSizeMult);
}
}
@Override
public int getCount() {
return filteredMarkers.size();
}
public void setData(List<Marker> filteredMarkers, SearchEngine.ReadyTokens rt) {
this.filteredMarkers = filteredMarkers;
this.rt = rt;
// set up empty view to make sure it does not show loading progress again
tEmpty.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
notifyDataSetChanged();
}
}
}