() {
@Override
protected SpannableStringBuilder onExecute(final Context context, final Bundle args) throws IOException {
final TupleMessageEx message = (TupleMessageEx) args.getSerializable("message");
final boolean show_images = args.getBoolean("show_images");
boolean show_quotes = args.getBoolean("show_quotes");
int zoom = args.getInt("zoom");
if (message == null || !message.content)
return null;
File file = message.getFile(context);
if (!file.exists())
return null;
String body = Helper.readText(file);
if (!show_quotes) {
Document document = Jsoup.parse(body);
for (Element quote : document.select("blockquote"))
quote.html("…");
body = document.html();
}
String html = HtmlHelper.sanitize(context, body, show_images);
if (debug)
html += "" + Html.escapeHtml(html) + "
";
Spanned spanned = HtmlHelper.fromHtml(html, new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
return HtmlHelper.decodeImage(context, message.id, source, show_images, tvBody);
}
}, null);
SpannableStringBuilder builder = new SpannableStringBuilder(spanned);
QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class);
for (QuoteSpan quoteSpan : quoteSpans) {
builder.setSpan(
new StyledQuoteSpan(colorPrimary),
builder.getSpanStart(quoteSpan),
builder.getSpanEnd(quoteSpan),
builder.getSpanFlags(quoteSpan));
builder.removeSpan(quoteSpan);
}
if (!show_quotes) {
final int px = Helper.dp2pixels(context, 24 + (zoom) * 8);
StyledQuoteSpan[] squotes = builder.getSpans(0, builder.length(), StyledQuoteSpan.class);
for (StyledQuoteSpan squote : squotes)
builder.setSpan(new DynamicDrawableSpan() {
@Override
public Drawable getDrawable() {
Drawable d = context.getDrawable(R.drawable.baseline_format_quote_24);
d.setTint(colorAccent);
d.setBounds(0, 0, px, px);
return d;
}
},
builder.getSpanStart(squote),
builder.getSpanEnd(squote),
builder.getSpanFlags(squote));
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean inline = prefs.getBoolean("inline_images", false);
boolean has_images;
ImageSpan[] spans = builder.getSpans(0, body.length(), ImageSpan.class);
if (inline) {
has_images = false;
for (ImageSpan span : spans) {
String source = span.getSource();
if (source == null || !source.startsWith("cid:")) {
has_images = true;
break;
}
}
} else
has_images = spans.length > 0;
args.putBoolean("has_images", has_images);
return builder;
}
@Override
protected void onExecuted(Bundle args, SpannableStringBuilder body) {
TupleMessageEx message = (TupleMessageEx) args.getSerializable("message");
properties.setBody(message.id, body);
TupleMessageEx amessage = getMessage();
if (amessage == null || !amessage.id.equals(message.id))
return;
boolean show_expanded = properties.getValue("expanded", message.id);
if (!show_expanded)
return;
boolean has_images = args.getBoolean("has_images");
boolean show_images = properties.getValue("images", message.id);
ibFull.setVisibility(hasWebView ? View.VISIBLE : View.GONE);
ibImages.setVisibility(has_images && !show_images ? View.VISIBLE : View.GONE);
tvBody.setText(body);
tvBody.setTextIsSelectable(false);
tvBody.setTextIsSelectable(true);
tvBody.setMovementMethod(new TouchHandler(message));
pbBody.setVisibility(View.GONE);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(parentFragment.getFragmentManager(), ex);
}
};
private class TouchHandler extends ArrowKeyMovementMethod {
private TupleMessageEx message;
TouchHandler(TupleMessageEx message) {
this.message = message;
}
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
boolean show_images = properties.getValue("images", message.id);
if (!show_images) {
ImageSpan[] image = buffer.getSpans(off, off, ImageSpan.class);
if (image.length > 0 && image[0].getSource() != null) {
HtmlHelper.AnnotatedSource a = new HtmlHelper.AnnotatedSource(image[0].getSource());
Uri uri = Uri.parse(a.getSource());
if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme())) {
onOpenLink(uri, null);
return true;
}
}
}
URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
if (link.length > 0) {
String url = link[0].getURL();
Uri uri = Uri.parse(url);
if (uri.getScheme() == null)
uri = Uri.parse("https://" + url);
int start = buffer.getSpanStart(link[0]);
int end = buffer.getSpanEnd(link[0]);
String title = (start < 0 || end < 0 || end <= start
? null : buffer.subSequence(start, end).toString());
if (url.equals(title))
title = null;
onOpenLink(uri, title);
return true;
}
ImageSpan[] image = buffer.getSpans(off, off, ImageSpan.class);
if (image.length > 0) {
String source = image[0].getSource();
if (source != null) {
onOpenImage(message.id, source);
return true;
}
}
DynamicDrawableSpan[] ddss = buffer.getSpans(off, off, DynamicDrawableSpan.class);
if (ddss.length > 0) {
properties.setValue("quotes", message.id, true);
loadText(message);
}
}
return super.onTouchEvent(widget, buffer, event);
}
}
private void onOpenLink(final Uri uri, String title) {
Log.i("Opening uri=" + uri);
if (BuildConfig.APPLICATION_ID.equals(uri.getHost()) && "/activate/".equals(uri.getPath())) {
try {
if (ActivityBilling.activatePro(context, uri))
ToastEx.makeText(context, R.string.title_pro_valid, Toast.LENGTH_LONG).show();
else
ToastEx.makeText(context, R.string.title_pro_invalid, Toast.LENGTH_LONG).show();
} catch (NoSuchAlgorithmException ex) {
Log.e(ex);
Helper.unexpectedError(parentFragment.getFragmentManager(), ex);
}
} else {
if ("cid".equals(uri.getScheme()))
return;
Bundle args = new Bundle();
args.putParcelable("uri", uri);
args.putString("title", title);
FragmentDialogLink fragment = new FragmentDialogLink();
fragment.setArguments(args);
fragment.show(parentFragment.getFragmentManager(), "open:link");
}
}
private void onOpenImage(long id, String source) {
Log.i("Viewing image source=" + source);
Bundle args = new Bundle();
args.putLong("id", id);
args.putString("source", source);
FragmentDialogImage fragment = new FragmentDialogImage();
fragment.setArguments(args);
fragment.show(parentFragment.getFragmentManager(), "view:image");
}
private void onMenuEditAsNew(final TupleMessageEx message) {
Intent asnew = new Intent(context, ActivityCompose.class)
.putExtra("action", "editasnew")
.putExtra("reference", message.id);
context.startActivity(asnew);
}
private void onMenuUnseen(final TupleMessageEx message) {
Bundle args = new Bundle();
args.putLong("id", message.id);
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) {
long id = args.getLong("id");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
EntityOperation.queue(context, message, EntityOperation.SEEN, false);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onExecuted(Bundle args, Void ignored) {
properties.setValue("expanded", message.id, false);
notifyDataSetChanged();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(parentFragment.getFragmentManager(), ex);
}
}.execute(context, owner, args, "message:unseen");
}
private void onMenuColoredStar(final TupleMessageEx message) {
int color = (message.color == null ? Color.TRANSPARENT : message.color);
Bundle args = new Bundle();
args.putLong("id", message.id);
FragmentDialogColor fragment = new FragmentDialogColor();
fragment.initialize(R.string.title_flag_color, color, args, context);
fragment.setTargetFragment(parentFragment, FragmentMessages.REQUEST_MESSAGE_COLOR);
fragment.show(parentFragment.getFragmentManager(), "message:color");
}
private void onMenuDelete(final TupleMessageEx message) {
Bundle args = new Bundle();
args.putLong("id", message.id);
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) {
long id = args.getLong("id");
DB db = DB.getInstance(context);
db.message().deleteMessage(id);
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(parentFragment.getFragmentManager(), ex);
}
}.execute(context, owner, args, "message:delete");
}
private void onMenuJunk(final TupleMessageEx message) {
String who = MessageHelper.formatAddresses(message.from);
Bundle aargs = new Bundle();
aargs.putString("question", context.getString(R.string.title_ask_spam_who, who));
aargs.putLong("id", message.id);
FragmentDialogAsk ask = new FragmentDialogAsk();
ask.setArguments(aargs);
ask.setTargetFragment(parentFragment, FragmentMessages.REQUEST_MESSAGE_JUNK);
ask.show(parentFragment.getFragmentManager(), "message:junk");
}
private void onMenuResync(TupleMessageEx message) {
Bundle args = new Bundle();
args.putLong("id", message.id);
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) {
long id = args.getLong("id");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
if (message == null || message.uid == null)
return null;
db.message().deleteMessage(id);
EntityOperation.sync(context, message.folder, true);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(parentFragment.getFragmentManager(), ex);
}
}.execute(context, owner, args, "message:share");
}
private void onMenuCreateRule(TupleMessageEx message) {
Intent rule = new Intent(ActivityView.ACTION_EDIT_RULE);
rule.putExtra("account", message.account);
rule.putExtra("folder", message.folder);
if (message.from != null && message.from.length > 0)
rule.putExtra("sender", ((InternetAddress) message.from[0]).getAddress());
if (message.to != null && message.to.length > 0)
rule.putExtra("recipient", ((InternetAddress) message.to[0]).getAddress());
if (!TextUtils.isEmpty(message.subject))
rule.putExtra("subject", message.subject);
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(rule);
}
private void onMenuManageKeywords(TupleMessageEx message) {
Bundle args = new Bundle();
args.putLong("id", message.id);
args.putStringArray("keywords", message.keywords);
new SimpleTask() {
@Override
protected EntityFolder onExecute(Context context, Bundle args) {
long id = args.getLong("id");
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
return db.folder().getFolder(message.folder);
}
@Override
protected void onExecuted(final Bundle args, EntityFolder folder) {
if (folder == null)
return;
args.putStringArray("fkeywords", folder.keywords);
FragmentKeywordManage fragment = new FragmentKeywordManage();
fragment.setArguments(args);
fragment.show(parentFragment.getFragmentManager(), "keyword:manage");
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(parentFragment.getFragmentManager(), ex);
}
}.execute(context, owner, args, "message:keywords");
}
private void onMenuShare(TupleMessageEx message) {
Bundle args = new Bundle();
args.putLong("id", message.id);
new SimpleTask() {
@Override
protected String[] onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(id);
if (message == null || !message.content)
return null;
File file = message.getFile(context);
if (!file.exists())
return null;
String from = null;
if (message.from != null && message.from.length > 0)
from = ((InternetAddress) message.from[0]).getAddress();
String html = HtmlHelper.getText(Helper.readText(file));
return new String[]{from, message.subject, html};
}
@Override
protected void onExecuted(Bundle args, String[] text) {
if (text == null)
return;
Intent share = new Intent();
share.setAction(Intent.ACTION_SEND);
share.setType("text/plain");
if (!TextUtils.isEmpty(text[0]))
share.putExtra(Intent.EXTRA_EMAIL, new String[]{text[0]});
if (!TextUtils.isEmpty(text[1]))
share.putExtra(Intent.EXTRA_SUBJECT, text[1]);
if (!TextUtils.isEmpty(text[2]))
share.putExtra(Intent.EXTRA_TEXT, text[2]);
PackageManager pm = context.getPackageManager();
if (share.resolveActivity(pm) == null)
Snackbar.make(parentFragment.getView(),
R.string.title_no_viewer, Snackbar.LENGTH_LONG).show();
else
context.startActivity(share);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(parentFragment.getFragmentManager(), ex);
}
}.execute(context, owner, args, "message:share");
}
private void onMenuPrint(TupleMessageEx message) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean("print_html_confirmed", false)) {
Bundle args = new Bundle();
args.putLong("id", message.id);
Intent data = new Intent();
data.putExtra("args", args);
parentFragment.onActivityResult(FragmentMessages.REQUEST_PRINT, RESULT_OK, data);
return;
}
Bundle aargs = new Bundle();
aargs.putString("question", context.getString(R.string.title_ask_show_html));
aargs.putString("notagain", "print_html_confirmed");
aargs.putLong("id", message.id);
FragmentDialogAsk ask = new FragmentDialogAsk();
ask.setArguments(aargs);
ask.setTargetFragment(parentFragment, FragmentMessages.REQUEST_PRINT);
ask.show(parentFragment.getFragmentManager(), "message:print");
}
private void onMenuShowHeaders(TupleMessageEx message) {
boolean show_headers = !properties.getValue("headers", message.id);
properties.setValue("headers", message.id, show_headers);
if (show_headers && message.headers == null) {
grpHeaders.setVisibility(View.VISIBLE);
if (suitable)
pbHeaders.setVisibility(View.VISIBLE);
else
tvNoInternetHeaders.setVisibility(View.VISIBLE);
Bundle args = new Bundle();
args.putLong("id", message.id);
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) {
Long id = args.getLong("id");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
EntityOperation.queue(context, message, EntityOperation.HEADERS);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(parentFragment.getFragmentManager(), ex);
}
}.execute(context, owner, args, "message:headers");
} else
notifyDataSetChanged();
}
private void onMenuRaw(TupleMessageEx message) {
if (message.raw == null) {
Bundle args = new Bundle();
args.putLong("id", message.id);
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) {
Long id = args.getLong("id");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
EntityOperation.queue(context, message, EntityOperation.RAW);
db.message().setMessageRaw(message.id, false);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(parentFragment.getFragmentManager(), ex);
}
}.execute(context, owner, args, "message:raw");
} else {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(FragmentMessages.ACTION_STORE_RAW)
.putExtra("id", message.id));
}
}
ItemDetailsLookup.ItemDetails getItemDetails(@NonNull MotionEvent motionEvent) {
return new ItemDetailsMessage(this);
}
Long getKey() {
return getKeyAtPosition(getAdapterPosition());
}
}
AdapterMessage(Fragment parentFragment,
String type, ViewType viewType,
boolean compact, int zoom, String sort, boolean filter_duplicates,
final IProperties properties) {
this.parentFragment = parentFragment;
this.type = type;
this.viewType = viewType;
this.compact = compact;
this.zoom = zoom;
this.sort = sort;
this.filter_duplicates = filter_duplicates;
this.properties = properties;
this.context = parentFragment.getContext();
this.owner = parentFragment.getViewLifecycleOwner();
this.suitable = ConnectionHelper.getNetworkState(context).isSuitable();
this.inflater = LayoutInflater.from(context);
this.TF = Helper.getTimeInstance(context, SimpleDateFormat.SHORT);
this.DTF = Helper.getDateTimeInstance(context, SimpleDateFormat.LONG, SimpleDateFormat.LONG);
this.dp36 = Helper.dp2pixels(context, 36);
this.colorPrimary = Helper.resolveColor(context, R.attr.colorPrimary);
this.colorAccent = Helper.resolveColor(context, R.attr.colorAccent);
this.colorWarning = Helper.resolveColor(context, R.attr.colorWarning);
this.textColorPrimary = Helper.resolveColor(context, android.R.attr.textColorPrimary);
this.textColorSecondary = Helper.resolveColor(context, android.R.attr.textColorSecondary);
this.colorUnread = Helper.resolveColor(context, R.attr.colorUnread);
this.hasWebView = Helper.hasWebView(context);
this.contacts = Helper.hasPermission(context, Manifest.permission.READ_CONTACTS);
this.textSize = Helper.getTextSize(context, zoom);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
this.date = prefs.getBoolean("date", true);
this.threading = prefs.getBoolean("threading", true);
this.name_email = prefs.getBoolean("name_email", !compact);
this.subject_italic = prefs.getBoolean("subject_italic", true);
this.flags = prefs.getBoolean("flags", true);
this.preview = prefs.getBoolean("preview", false);
this.preview_italic = prefs.getBoolean("preview_italic", true);
this.attachments_alt = prefs.getBoolean("attachments_alt", false);
this.contrast = prefs.getBoolean("contrast", false);
this.monospaced = prefs.getBoolean("monospaced", false);
this.contact_images = (this.contacts && prefs.getBoolean("contact_images", true));
this.all_images = prefs.getBoolean("all_images", false);
this.collapse_quotes = prefs.getBoolean("collapse_quotes", false);
this.authentication = prefs.getBoolean("authentication", true);
debug = prefs.getBoolean("debug", false);
AsyncDifferConfig config = new AsyncDifferConfig.Builder<>(DIFF_CALLBACK)
.build();
this.differ = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
this.differ.addPagedListListener(new AsyncPagedListDiffer.PagedListListener() {
@Override
public void onCurrentListChanged(@Nullable PagedList previousList, @Nullable PagedList currentList) {
if (gotoTop) {
gotoTop = false;
properties.scrollTo(0);
}
}
});
owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
Log.i(AdapterMessage.this + " parent destroyed");
AdapterMessage.this.parentFragment = null;
}
});
}
void gotoTop() {
properties.scrollTo(0);
this.gotoTop = true;
}
void submitList(PagedList list) {
keyPosition.clear();
differ.submitList(list);
}
PagedList getCurrentList() {
return differ.getCurrentList();
}
void setCompact(boolean compact) {
if (this.compact != compact) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
this.compact = compact;
this.name_email = prefs.getBoolean("name_email", !compact);
notifyDataSetChanged();
}
}
void setZoom(int zoom) {
if (this.zoom != zoom) {
this.zoom = zoom;
textSize = Helper.getTextSize(context, zoom);
notifyDataSetChanged();
}
}
int getZoom() {
return this.zoom;
}
void setSort(String sort) {
if (!sort.equals(this.sort)) {
this.sort = sort;
notifyDataSetChanged();
// Needed to redraw item decorators / add/remove size
}
}
String getSort() {
return this.sort;
}
void setFilterDuplicates(boolean filter_duplicates) {
if (this.filter_duplicates != filter_duplicates) {
this.filter_duplicates = filter_duplicates;
notifyDataSetChanged();
}
}
void checkInternet() {
boolean suitable = ConnectionHelper.getNetworkState(context).isSuitable();
if (this.suitable != suitable) {
this.suitable = suitable;
notifyDataSetChanged();
}
}
void setAnswerCount(int answers) {
this.answers = answers;
Log.i("Answer count=" + answers);
}
@Override
public int getItemViewType(int position) {
return (compact ? R.layout.item_message_compact : R.layout.item_message_normal);
}
@Override
public int getItemCount() {
return differ.getItemCount();
}
private static final DiffUtil.ItemCallback DIFF_CALLBACK =
new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(
@NonNull TupleMessageEx prev, @NonNull TupleMessageEx next) {
return prev.id.equals(next.id);
}
@Override
public boolean areContentsTheSame(
@NonNull TupleMessageEx prev, @NonNull TupleMessageEx next) {
boolean same = true;
// id
// account
// folder
if (!Objects.equals(prev.identity, next.identity)) {
// via
same = false;
Log.i("Entity changed id=" + next.id);
}
// extra
if (!Objects.equals(prev.uid, next.uid)) {
same = false;
Log.i("uid changed id=" + next.id);
}
if (!Objects.equals(prev.msgid, next.msgid)) {
// debug info
same = false;
Log.i("msgid changed id=" + next.id);
}
// references
// deliveredto
// inreplyto
if (!Objects.equals(prev.thread, next.thread)) {
same = false;
Log.i("thread changed id=" + next.id);
}
// receipt_request
if (!MessageHelper.equal(prev.receipt_to, next.receipt_to)) {
same = false;
Log.i("receipt_to changed id=" + next.id);
}
if (!Objects.equals(prev.dkim, next.dkim)) {
same = false;
Log.i("dkim changed id=" + next.id);
}
if (!Objects.equals(prev.spf, next.spf)) {
same = false;
Log.i("spf changed id=" + next.id);
}
if (!Objects.equals(prev.dmarc, next.dmarc)) {
same = false;
Log.i("dmarc changed id=" + next.id);
}
if (!Objects.equals(prev.mx, next.mx)) {
same = false;
Log.i("mx changed id=" + next.id);
}
if (!Objects.equals(prev.avatar, next.avatar)) {
same = false;
Log.i("avatar changed id=" + next.id);
}
if (!Objects.equals(prev.sender, next.sender)) {
same = false;
Log.i("sender changed id=" + next.id);
}
if (!MessageHelper.equal(prev.from, next.from)) {
same = false;
Log.i("from changed id=" + next.id);
}
if (!MessageHelper.equal(prev.to, next.to)) {
same = false;
Log.i("to changed id=" + next.id);
}
if (!MessageHelper.equal(prev.cc, next.cc)) {
same = false;
Log.i("cc changed id=" + next.id);
}
if (!MessageHelper.equal(prev.bcc, next.bcc)) {
same = false;
Log.i("bcc changed id=" + next.id);
}
if (!MessageHelper.equal(prev.reply, next.reply)) {
same = false;
Log.i("reply changed id=" + next.id);
}
if (!MessageHelper.equal(prev.list_post, next.list_post)) {
same = false;
Log.i("list_post changed id=" + next.id);
}
if (!Objects.equals(prev.headers, next.headers)) {
same = false;
Log.i("headers changed id=" + next.id);
}
if (!Objects.equals(prev.raw, next.raw)) {
same = false;
Log.i("raw changed id=" + next.id);
}
if (!Objects.equals(prev.subject, next.subject)) {
same = false;
Log.i("subject changed id=" + next.id);
}
if (!Objects.equals(prev.size, next.size)) {
same = false;
Log.i("size changed id=" + next.id);
}
if (!Objects.equals(prev.attachments, next.attachments)) {
same = false;
Log.i("attachments changed id=" + next.id);
}
if (!prev.content.equals(next.content)) {
same = false;
Log.i("content changed id=" + next.id);
}
// plain_only
if (!Objects.equals(prev.preview, next.preview)) {
same = false;
Log.i("preview changed id=" + next.id);
}
if (!Objects.equals(prev.sent, next.sent)) {
same = false;
Log.i("sent changed id=" + next.id);
}
if (!prev.received.equals(next.received)) {
same = false;
Log.i("received changed id=" + next.id);
}
if (!prev.stored.equals(next.stored)) {
// updated after decryption
same = false;
Log.i("stored changed id=" + next.id);
}
// seen
// answered
// flagged
if (debug && !Objects.equals(prev.flags, next.flags)) {
same = false;
Log.i("flags changed id=" + next.id);
}
if (!Helper.equal(prev.keywords, next.keywords)) {
same = false;
Log.i("keywords changed id=" + next.id);
}
if (!prev.ui_seen.equals(next.ui_seen)) {
same = false;
Log.i("ui_seen changed id=" + next.id);
}
if (!prev.ui_answered.equals(next.ui_answered)) {
same = false;
Log.i("ui_answer changed id=" + next.id);
}
if (!prev.ui_flagged.equals(next.ui_flagged)) {
same = false;
Log.i("ui_flagged changed id=" + next.id);
}
if (!prev.ui_hide.equals(next.ui_hide)) {
same = false;
Log.i("ui_hide changed id=" + next.id);
}
if (!prev.ui_found.equals(next.ui_found)) {
same = false;
Log.i("ui_found changed id=" + next.id);
}
// ui_ignored
if (!prev.ui_browsed.equals(next.ui_browsed)) {
same = false;
Log.i("ui_browsed changed id=" + next.id);
}
if (!Objects.equals(prev.ui_snoozed, next.ui_snoozed)) {
same = false;
Log.i("ui_snoozed changed id=" + next.id);
}
if (!Objects.equals(prev.color, next.color)) {
same = false;
Log.i("color changed id=" + next.id);
}
// revision
// revisions
if (!Objects.equals(prev.warning, next.warning)) {
same = false;
Log.i("warning changed id=" + next.id);
}
if (!Objects.equals(prev.error, next.error)) {
same = false;
Log.i("error changed id=" + next.id);
}
// last_attempt
if (!Objects.equals(prev.accountName, next.accountName)) {
same = false;
Log.i("accountName changed id=" + next.id);
}
if (!Objects.equals(prev.accountColor, next.accountColor)) {
same = false;
Log.i("accountColor changed id=" + next.id);
}
// accountNotify
if (!prev.folderName.equals(next.folderName)) {
same = false;
Log.i("folderName changed id=" + next.id);
}
if (!Objects.equals(prev.folderDisplay, next.folderDisplay)) {
same = false;
Log.i("folderDisplay changed id=" + next.id);
}
if (!prev.folderType.equals(next.folderType)) {
same = false;
Log.i("folderType changed id=" + next.id);
}
if (!Objects.equals(prev.identityName, next.identityName)) {
same = false;
Log.i("identityName changed id=" + next.id);
}
if (!Objects.equals(prev.identityEmail, next.identityEmail)) {
same = false;
Log.i("identityEmail changed id=" + next.id);
}
if (!Objects.equals(prev.identitySynchronize, next.identitySynchronize)) {
same = false;
Log.i("identitySynchronize changed id=" + next.id);
}
if (prev.count != next.count) {
same = false;
Log.i("count changed id=" + next.id);
}
if (prev.unseen != next.unseen) {
same = false;
Log.i("unseen changed id=" + next.id);
}
if (prev.unflagged != next.unflagged) {
same = false;
Log.i("unflagged changed id=" + next.id);
}
if (prev.attachments != next.attachments) {
same = false;
Log.i("attachments changed id=" + next.id);
}
if (prev.drafts != next.drafts) {
same = false;
Log.i("drafts changed id=" + next.id);
}
if (prev.visible != next.visible) {
same = false;
Log.i("visible changed id=" + next.id);
}
if (!Objects.equals(prev.totalSize, next.totalSize)) {
same = false;
Log.i("totalSize changed id=" + next.id);
}
if (prev.duplicate != next.duplicate) {
same = false;
Log.i("duplicate changed id=" + next.id);
}
return same;
}
};
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(viewType, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire();
TupleMessageEx message = differ.getItem(position);
if (message == null || context == null)
holder.clear();
else {
holder.bindTo(message);
holder.wire();
}
}
@Override
public void onViewRecycled(@NonNull ViewHolder holder) {
holder.cowner.stop();
holder.powner.recreate();
}
void setSelectionTracker(SelectionTracker selectionTracker) {
this.selectionTracker = selectionTracker;
}
int getPositionForKey(long key) {
if (keyPosition.isEmpty()) {
PagedList messages = getCurrentList();
if (messages != null) {
for (int i = 0; i < messages.size(); i++) {
TupleMessageEx message = messages.get(i);
if (message != null)
keyPosition.put(message.id, i);
}
Log.i("Mapped keys=" + keyPosition.size());
}
}
if (keyPosition.containsKey(key)) {
int pos = keyPosition.get(key);
Log.d("Position=" + pos + " @Key=" + key);
return pos;
}
Log.i("Position=" + RecyclerView.NO_POSITION + " @Key=" + key);
return RecyclerView.NO_POSITION;
}
TupleMessageEx getItemAtPosition(int pos) {
PagedList messages = getCurrentList();
if (messages != null && pos >= 0 && pos < messages.size()) {
TupleMessageEx message = messages.get(pos);
Long key = (message == null ? null : message.id);
Log.d("Item=" + key + " @Position=" + pos);
return message;
} else {
Log.d("Item=" + null + " @Position=" + pos);
return null;
}
}
TupleMessageEx getItemForKey(long key) {
int pos = getPositionForKey(key);
if (pos == RecyclerView.NO_POSITION) {
Log.d("Item=" + null + " @Key=" + key);
return null;
} else
return getItemAtPosition(pos);
}
Long getKeyAtPosition(int pos) {
TupleMessageEx message = getItemAtPosition(pos);
Long key = (message == null ? null : message.id);
Log.d("Key=" + key + " @Position=" + pos);
return key;
}
interface IProperties {
void setValue(String name, long id, boolean enabled);
boolean getValue(String name, long id);
void setBody(long id, Spanned body);
Spanned getBody(long id);
void setAttchments(long id, List attachments);
List getAttachments(long id);
void scrollTo(int pos);
void scrollBy(int dx, int dy);
void move(long id, String type);
void finish();
}
public static class FragmentDialogLink extends FragmentDialogEx {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final Uri uri = getArguments().getParcelable("uri");
String title = getArguments().getString("title");
final Uri sanitized;
if (uri.isOpaque())
sanitized = uri;
else {
// https://en.wikipedia.org/wiki/UTM_parameters
Uri.Builder builder = uri.buildUpon();
boolean changed = false;
builder.clearQuery();
for (String key : uri.getQueryParameterNames())
if (PARANOID_QUERY.contains(key.toLowerCase()))
changed = true;
else if (!TextUtils.isEmpty(key))
for (String value : uri.getQueryParameters(key)) {
Log.i("Query " + key + "=" + value);
builder.appendQueryParameter(key, value);
}
sanitized = (changed ? builder.build() : uri);
}
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_open_link, null);
TextView tvTitle = view.findViewById(R.id.tvTitle);
final EditText etLink = view.findViewById(R.id.etLink);
final CheckBox cbSecure = view.findViewById(R.id.cbSecure);
CheckBox cbSanitize = view.findViewById(R.id.cbSanitize);
final Button btnOwner = view.findViewById(R.id.btnOwner);
TextView tvOwnerRemark = view.findViewById(R.id.tvOwnerRemark);
final ContentLoadingProgressBar pbWait = view.findViewById(R.id.pbWait);
final TextView tvOwner = view.findViewById(R.id.tvOwner);
final TextView tvHost = view.findViewById(R.id.tvHost);
final Group grpOwner = view.findViewById(R.id.grpOwner);
tvTitle.setText(title);
tvTitle.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
cbSecure.setVisibility(View.GONE);
etLink.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence text, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable text) {
Uri uri = Uri.parse(text.toString());
cbSecure.setVisibility(!uri.isOpaque() &&
("http".equals(uri.getScheme()) || "https".equals(uri.getScheme()))
? View.VISIBLE : View.GONE);
}
});
etLink.setText(uri.toString());
boolean secure = "https".equals(uri.getScheme());
cbSecure.setChecked(secure);
cbSecure.setText(
secure ? R.string.title_link_secured : R.string.title_secure_link);
cbSecure.setTextColor(Helper.resolveColor(getContext(),
secure ? android.R.attr.textColorSecondary : R.attr.colorWarning));
cbSecure.setTypeface(
secure ? Typeface.DEFAULT : Typeface.DEFAULT_BOLD);
cbSecure.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
Uri uri = Uri.parse(etLink.getText().toString());
Uri.Builder builder = uri.buildUpon();
builder.scheme(checked ? "https" : "http");
String authority = uri.getEncodedAuthority();
if (authority != null) {
authority = authority.replace(checked ? ":80" : ":443", checked ? ":443" : ":80");
builder.encodedAuthority(authority);
}
etLink.setText(builder.build().toString());
cbSecure.setText(
checked ? R.string.title_link_secured : R.string.title_secure_link);
cbSecure.setTextColor(Helper.resolveColor(getContext(),
checked ? android.R.attr.textColorSecondary : R.attr.colorWarning));
cbSecure.setTypeface(
checked ? Typeface.DEFAULT : Typeface.DEFAULT_BOLD);
}
});
cbSanitize.setVisibility(uri.equals(sanitized) ? View.GONE : View.VISIBLE);
cbSanitize.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
if (checked)
etLink.setText(sanitized.toString());
else
etLink.setText(uri.toString());
}
});
tvOwnerRemark.setMovementMethod(LinkMovementMethod.getInstance());
pbWait.setVisibility(View.GONE);
grpOwner.setVisibility(View.GONE);
btnOwner.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Bundle args = new Bundle();
args.putParcelable("uri", uri);
new SimpleTask() {
@Override
protected void onPreExecute(Bundle args) {
btnOwner.setEnabled(false);
pbWait.setVisibility(View.VISIBLE);
grpOwner.setVisibility(View.GONE);
}
@Override
protected void onPostExecute(Bundle args) {
btnOwner.setEnabled(true);
pbWait.setVisibility(View.GONE);
grpOwner.setVisibility(View.VISIBLE);
}
@Override
protected String[] onExecute(Context context, Bundle args) throws Throwable {
Uri uri = args.getParcelable("uri");
return IPInfo.getOrganization(uri);
}
@Override
protected void onExecuted(Bundle args, String[] data) {
String host = data[0];
String organization = data[1];
tvHost.setText(host);
tvOwner.setText(organization == null ? "?" : organization);
}
@Override
protected void onException(Bundle args, Throwable ex) {
tvOwner.setText(ex.getMessage());
}
}.execute(getContext(), getActivity(), args, "link:owner");
}
});
return new AlertDialog.Builder(getContext())
.setView(view)
.setPositiveButton(R.string.title_yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Uri uri = Uri.parse(etLink.getText().toString());
Helper.view(getContext(), uri, false);
}
})
.setNeutralButton(R.string.title_browse, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Uri uri = Uri.parse(etLink.getText().toString());
Helper.view(getContext(), uri, true);
}
})
.setNegativeButton(R.string.title_no, null)
.create();
}
}
public static class FragmentDialogImage extends FragmentDialogEx {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final PhotoView pv = new PhotoView(getContext());
new SimpleTask() {
@Override
protected Drawable onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
String source = args.getString("source");
return HtmlHelper.decodeImage(context, id, source, true, null);
}
@Override
protected void onExecuted(Bundle args, Drawable drawable) {
pv.setImageDrawable(drawable);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getFragmentManager(), ex);
}
}.execute(getContext(), getActivity(), getArguments(), "view:image");
final Dialog dialog = new Dialog(getContext(), android.R.style.Theme_Black_NoTitleBar_Fullscreen);
dialog.setContentView(pv);
return dialog;
}
}
public static class FragmentDialogFull extends FragmentDialogEx {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ask_again, null);
final TextView tvMessage = dview.findViewById(R.id.tvMessage);
final CheckBox cbNotAgain = dview.findViewById(R.id.cbNotAgain);
tvMessage.setText(getText(R.string.title_ask_show_html));
return new AlertDialog.Builder(getContext())
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
getArguments().putString("name", "confirmed");
getArguments().putBoolean("value", true);
if (cbNotAgain.isChecked()) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
prefs.edit().putBoolean("show_html_confirmed", true).apply();
}
FragmentDialogWebView fragment = new FragmentDialogWebView();
fragment.setArguments(getArguments());
fragment.show(getFragmentManager(), "message:full");
sendResult(RESULT_OK);
}
})
.create();
}
}
public static class FragmentDialogWebView extends FragmentDialogEx {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
boolean show_images = getArguments().getBoolean("show_images");
float textSize = getArguments().getFloat("text_size");
final View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_webview, null);
final WebView webView = dview.findViewById(R.id.webView);
final ContentLoadingProgressBar pbWait = dview.findViewById(R.id.pbWait);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean inline = prefs.getBoolean("inline_images", false);
boolean autocontent = prefs.getBoolean("autocontent", false);
setupWebView(webView);
WebSettings settings = webView.getSettings();
settings.setDefaultFontSize(Math.round(textSize));
settings.setDefaultFixedFontSize(Math.round(textSize));
settings.setLoadsImagesAutomatically(show_images || inline);
settings.setBlockNetworkLoads(!show_images && !autocontent);
settings.setBlockNetworkImage(!show_images && !autocontent);
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
Dialog dialog = new Dialog(getContext(), android.R.style.Theme_Light_NoTitleBar_Fullscreen);
dialog.setContentView(dview);
new SimpleTask() {
@Override
protected void onPreExecute(Bundle args) {
webView.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
}
@Override
protected void onPostExecute(Bundle args) {
pbWait.setVisibility(View.GONE);
}
@Override
protected String onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(id);
if (message == null || !message.content)
return null;
File file = message.getFile(context);
if (!file.exists())
return null;
String html = HtmlHelper.getHtmlEmbedded(context, id, Helper.readText(file));
// Remove viewport limitations
Document doc = Jsoup.parse(html);
for (Element meta : doc.select("meta").select("[name=viewport]")) {
String content = meta.attr("content");
String[] params = content.split(";");
if (params.length > 0) {
List viewport = new ArrayList<>();
for (String param : params)
if (!param.toLowerCase().contains("maximum-scale") &&
!param.toLowerCase().contains("user-scalable"))
viewport.add(param.trim());
if (viewport.size() == 0)
meta.attr("content", "");
else
meta.attr("content", TextUtils.join(" ;", viewport) + ";");
}
}
return doc.html();
}
@Override
protected void onExecuted(Bundle args, String html) {
webView.loadDataWithBaseURL("", html, "text/html", "UTF-8", null);
webView.setVisibility(View.VISIBLE);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getFragmentManager(), ex);
}
}.execute(getContext(), getActivity(), getArguments(), "message:full");
return dialog;
}
private void setupWebView(WebView webView) {
webView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.i("Open url=" + url);
Uri uri = Uri.parse(url);
if ("cid".equals(uri.getScheme()) || "data".equals(uri.getScheme()))
return false;
FragmentManager manager = getFragmentManager();
if (manager == null)
return false;
Bundle args = new Bundle();
args.putParcelable("uri", uri);
args.putString("title", null);
FragmentDialogLink fragment = new FragmentDialogLink();
fragment.setArguments(args);
fragment.show(manager, "open:link");
return true;
}
});
webView.setDownloadListener(new DownloadListener() {
public void onDownloadStart(
String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
Log.i("Download url=" + url + " mime type=" + mimetype);
Uri uri = Uri.parse(url);
if ("cid".equals(uri.getScheme()) || "data".equals(uri.getScheme()))
return;
Helper.view(getContext(), uri, true);
}
});
webView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
WebView.HitTestResult result = ((WebView) view).getHitTestResult();
if (result.getType() == WebView.HitTestResult.IMAGE_TYPE ||
result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
Log.i("Long press url=" + result.getExtra());
Uri uri = Uri.parse(result.getExtra());
if ("cid".equals(uri.getScheme()) || "data".equals(uri.getScheme()))
return false;
Helper.view(getContext(), uri, true);
return true;
}
return false;
}
});
WebSettings settings = webView.getSettings();
settings.setUseWideViewPort(true);
settings.setLoadWithOverviewMode(true);
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
settings.setAllowFileAccess(false);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean monospaced = prefs.getBoolean("monospaced", false);
if (monospaced)
settings.setStandardFontFamily("monospace");
}
}
public static class FragmentKeywordManage extends FragmentDialogEx {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final long id = getArguments().getLong("id");
List keywords = Arrays.asList(getArguments().getStringArray("keywords"));
List fkeywords = Arrays.asList(getArguments().getStringArray("fkeywords"));
final List items = new ArrayList<>(keywords);
for (String keyword : fkeywords)
if (!items.contains(keyword))
items.add(keyword);
Collections.sort(items);
final boolean[] selected = new boolean[items.size()];
final boolean[] dirty = new boolean[items.size()];
for (int i = 0; i < selected.length; i++) {
selected[i] = keywords.contains(items.get(i));
dirty[i] = false;
}
return new AlertDialog.Builder(getContext())
.setTitle(R.string.title_manage_keywords)
.setMultiChoiceItems(items.toArray(new String[0]), selected, new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
dirty[which] = true;
}
})
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (!ActivityBilling.isPro(getContext())) {
getContext().startActivity(new Intent(getContext(), ActivityBilling.class));
return;
}
Bundle args = new Bundle();
args.putLong("id", id);
args.putStringArray("keywords", items.toArray(new String[0]));
args.putBooleanArray("selected", selected);
args.putBooleanArray("dirty", dirty);
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) {
long id = args.getLong("id");
String[] keywords = args.getStringArray("keywords");
boolean[] selected = args.getBooleanArray("selected");
boolean[] dirty = args.getBooleanArray("dirty");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
for (int i = 0; i < selected.length; i++)
if (dirty[i])
EntityOperation.queue(context, message, EntityOperation.KEYWORD, keywords[i], selected[i]);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getFragmentManager(), ex);
}
}.execute(getContext(), getActivity(), args, "message:keywords:manage");
}
})
.setNeutralButton(R.string.title_add, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Bundle args = new Bundle();
args.putLong("id", id);
FragmentKeywordAdd fragment = new FragmentKeywordAdd();
fragment.setArguments(args);
fragment.show(getFragmentManager(), "keyword:add");
}
})
.create();
}
}
public static class FragmentKeywordAdd extends FragmentDialogEx {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final long id = getArguments().getLong("id");
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_keyword, null);
final EditText etKeyword = view.findViewById(R.id.etKeyword);
etKeyword.setText(null);
return new AlertDialog.Builder(getContext())
.setView(view)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (!ActivityBilling.isPro(getContext())) {
getContext().startActivity(new Intent(getContext(), ActivityBilling.class));
return;
}
String keyword = Helper.sanitizeKeyword(etKeyword.getText().toString());
if (!TextUtils.isEmpty(keyword)) {
Bundle args = new Bundle();
args.putLong("id", id);
args.putString("keyword", keyword);
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) {
long id = args.getLong("id");
String keyword = args.getString("keyword");
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
EntityOperation.queue(context, message, EntityOperation.KEYWORD, keyword, true);
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getFragmentManager(), ex);
}
}.execute(getContext(), getActivity(), args, "message:keyword:add");
}
}
}).create();
}
}
}