() {
@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);
if (debug)
html += "" + Html.escapeHtml(html) + "
";
Spanned spanned = HtmlHelper.fromHtml(html, new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
Drawable image = HtmlHelper.decodeImage(source, message.id, show_images, tvBody);
ConstraintLayout.LayoutParams params =
(ConstraintLayout.LayoutParams) tvBody.getLayoutParams();
float width = context.getResources().getDisplayMetrics().widthPixels
- params.leftMargin - params.rightMargin;
if (image.getIntrinsicWidth() > width) {
float scale = width / image.getIntrinsicWidth();
image.setBounds(0, 0,
Math.round(image.getIntrinsicWidth() * scale),
Math.round(image.getIntrinsicHeight() * scale));
}
return image;
}
}, 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));
}
args.putBoolean("has_images", builder.getSpans(0, body.length(), ImageSpan.class).length > 0);
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);
tbHtml.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(context, owner, 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) {
Uri uri = Uri.parse(image[0].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 && image[0].getSource() != null) {
onOpenImage(image[0].getDrawable());
return true;
}
DynamicDrawableSpan[] ddss = buffer.getSpans(off, off, DynamicDrawableSpan.class);
if (ddss.length > 0) {
properties.setValue("quotes", message.id, true);
showText(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())) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_ACTIVATE_PRO)
.putExtra("uri", uri));
} else {
if ("cid".equals(uri.getScheme()))
return;
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean paranoid = prefs.getBoolean("paranoid", true);
final Uri _uri;
if (paranoid && !uri.isOpaque()) {
// https://en.wikipedia.org/wiki/UTM_parameters
Uri.Builder builder = new Uri.Builder();
String scheme = uri.getScheme();
if (!TextUtils.isEmpty(scheme))
builder.scheme(scheme);
String authority = uri.getEncodedAuthority();
if (!TextUtils.isEmpty(authority))
builder.encodedAuthority(authority);
String path = uri.getEncodedPath();
if (!TextUtils.isEmpty(path))
builder.encodedPath(path);
for (String key : uri.getQueryParameterNames())
if (!PARANOID_QUERY.contains(key.toLowerCase()))
for (String value : uri.getQueryParameters(key))
builder.appendQueryParameter(key, value);
String fragment = uri.getEncodedFragment();
if (!TextUtils.isEmpty(fragment))
builder.encodedFragment(fragment);
_uri = builder.build();
Log.i("Source uri=" + uri);
Log.i("Target uri=" + _uri);
} else
_uri = uri;
View view = LayoutInflater.from(context).inflate(R.layout.dialog_open_link, null);
TextView tvTitle = view.findViewById(R.id.tvTitle);
final EditText etLink = view.findViewById(R.id.etLink);
TextView tvInsecure = view.findViewById(R.id.tvInsecure);
final TextView tvOwner = view.findViewById(R.id.tvOwner);
Group grpOwner = view.findViewById(R.id.grpOwner);
tvTitle.setText(title);
tvTitle.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
etLink.setText(_uri.toString());
tvInsecure.setVisibility("http".equals(_uri.getScheme()) ? View.VISIBLE : View.GONE);
grpOwner.setVisibility(paranoid ? View.VISIBLE : View.GONE);
if (paranoid) {
Bundle args = new Bundle();
args.putParcelable("uri", _uri);
new SimpleTask() {
@Override
protected void onPreExecute(Bundle args) {
tvOwner.setText("…");
}
@Override
protected String onExecute(Context context, Bundle args) throws Throwable {
Uri uri = args.getParcelable("uri");
String host = uri.getHost();
return (TextUtils.isEmpty(host) ? null : IPInfo.getOrganization(host));
}
@Override
protected void onExecuted(Bundle args, String organization) {
tvOwner.setText(organization == null ? "?" : organization);
}
@Override
protected void onException(Bundle args, Throwable ex) {
tvOwner.setText(ex.getMessage());
}
}.execute(context, owner, args, "link:domain");
}
new DialogBuilderLifecycle(context, owner)
.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(context, owner, 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(context, owner, uri, true);
}
})
.setNegativeButton(R.string.title_no, null)
.show();
}
}
private void onOpenImage(Drawable drawable) {
PhotoView pv = new PhotoView(context);
pv.setImageDrawable(drawable);
final Dialog dialog = new Dialog(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
dialog.setContentView(pv);
owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void onCreate() {
dialog.show();
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
dialog.dismiss();
}
});
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
ActionData data = (ActionData) bnvActions.getTag();
if (data == null)
return false;
switch (item.getItemId()) {
case R.id.action_more:
onActionMore(data);
return true;
case R.id.action_delete:
onActionDelete(data);
return true;
case R.id.action_move:
onActionMove(data);
return true;
case R.id.action_archive:
onActionArchive(data);
return true;
case R.id.action_reply:
onActionReplyMenu(data);
return true;
default:
return false;
}
}
private void onMenuForward(final ActionData data) {
Intent forward = new Intent(context, ActivityCompose.class)
.putExtra("action", "forward")
.putExtra("reference", data.message.id);
context.startActivity(forward);
}
private void onMenuUnseen(final ActionData data) {
Bundle args = new Bundle();
args.putLong("id", data.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", data.message.id, false);
notifyDataSetChanged();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:unseen");
}
private void onMenuColoredStar(final ActionData data) {
Intent color = new Intent(ActivityView.ACTION_COLOR);
color.putExtra("id", data.message.id);
color.putExtra("color", data.message.color == null ? Color.TRANSPARENT : data.message.color);
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(color);
}
private void onMenuCopy(final ActionData data) {
Bundle args = new Bundle();
args.putLong("id", data.message.id);
new SimpleTask>() {
@Override
protected List onExecute(Context context, Bundle args) {
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(args.getLong("id"));
if (message == null)
return null;
List folders = db.folder().getFolders(message.account);
if (folders == null)
return null;
if (folders.size() > 0)
Collections.sort(folders, folders.get(0).getComparator(context));
return folders;
}
@Override
protected void onExecuted(final Bundle args, List folders) {
if (folders == null)
return;
View anchor = bnvActions.findViewById(R.id.action_more);
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, anchor);
int order = 0;
for (EntityFolder folder : folders)
popupMenu.getMenu().add(Menu.NONE, folder.id.intValue(), order++, folder.getDisplayName(context));
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(final MenuItem target) {
args.putLong("target", target.getItemId());
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) {
long id = args.getLong("id");
long target = args.getLong("target");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
EntityOperation.queue(context, message, EntityOperation.COPY, target);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:copy");
return true;
}
});
popupMenu.show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:copy:list");
}
private void onMenuDelete(final ActionData data) {
Bundle args = new Bundle();
args.putLong("id", data.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(context, owner, ex);
}
}.execute(context, owner, args, "message:delete");
}
private void onMenuJunk(final ActionData data) {
String who = MessageHelper.formatAddresses(data.message.from);
new DialogBuilderLifecycle(context, owner)
.setMessage(context.getString(R.string.title_ask_spam_who, who))
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Bundle args = new Bundle();
args.putLong("id", data.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;
EntityFolder junk = db.folder().getFolderByType(message.account, EntityFolder.JUNK);
EntityOperation.queue(context, message, EntityOperation.MOVE, junk.id);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:spam");
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void onMenuDecrypt(ActionData data) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_DECRYPT)
.putExtra("id", data.message.id));
}
private void onMenuResync(ActionData data) {
Bundle args = new Bundle();
args.putLong("id", data.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, false);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:share");
}
private void onMenuCreateRule(ActionData data) {
Intent rule = new Intent(ActivityView.ACTION_EDIT_RULE);
rule.putExtra("account", data.message.account);
rule.putExtra("folder", data.message.folder);
if (data.message.from != null && data.message.from.length > 0)
rule.putExtra("sender", ((InternetAddress) data.message.from[0]).getAddress());
if (data.message.to != null && data.message.to.length > 0)
rule.putExtra("recipient", ((InternetAddress) data.message.to[0]).getAddress());
if (!TextUtils.isEmpty(data.message.subject))
rule.putExtra("subject", data.message.subject);
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(rule);
}
private void onMenuManageKeywords(ActionData data) {
if (!Helper.isPro(context)) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(new Intent(ActivityView.ACTION_SHOW_PRO));
return;
}
Bundle args = new Bundle();
args.putSerializable("message", data.message);
new SimpleTask() {
@Override
protected EntityFolder onExecute(Context context, Bundle args) {
EntityMessage message = (EntityMessage) args.getSerializable("message");
return DB.getInstance(context).folder().getFolder(message.folder);
}
@Override
protected void onExecuted(final Bundle args, EntityFolder folder) {
EntityMessage message = (EntityMessage) args.getSerializable("message");
List keywords = Arrays.asList(message.keywords);
final List items = new ArrayList<>(keywords);
for (String keyword : folder.keywords)
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;
}
new DialogBuilderLifecycle(context, owner)
.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) {
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) {
EntityMessage message = (EntityMessage) args.getSerializable("message");
String[] keywords = args.getStringArray("keywords");
boolean[] selected = args.getBooleanArray("selected");
boolean[] dirty = args.getBooleanArray("dirty");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
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(context, owner, ex);
}
}.execute(context, owner, args, "message:keywords:managa");
}
})
.setNeutralButton(R.string.title_add, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_keyword, null);
final EditText etKeyword = view.findViewById(R.id.etKeyword);
etKeyword.setText(null);
new DialogBuilderLifecycle(context, owner)
.setView(view)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String keyword = Helper.sanitizeKeyword(etKeyword.getText().toString());
if (!TextUtils.isEmpty(keyword)) {
args.putString("keyword", keyword);
new SimpleTask() {
@Override
protected Void onExecute(Context context, Bundle args) {
EntityMessage message = (EntityMessage) args.getSerializable("message");
String keyword = args.getString("keyword");
EntityOperation.queue(context, message, EntityOperation.KEYWORD, keyword, true);
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:keyword:add");
}
}
}).show();
}
})
.show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:keywords");
}
private void onMenuShare(ActionData data) {
Bundle args = new Bundle();
args.putLong("id", data.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)
Toast.makeText(context, R.string.title_no_viewer, Toast.LENGTH_LONG).show();
else
context.startActivity(share);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:share");
}
private void onMenuPrint(final ActionData data) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean("print_html_confirmed", false)) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_PRINT)
.putExtra("id", data.message.id));
return;
}
final View dview = LayoutInflater.from(context).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(context.getText(R.string.title_ask_show_html));
new DialogBuilderLifecycle(context, owner)
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (cbNotAgain.isChecked())
prefs.edit().putBoolean("print_html_confirmed", true).apply();
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_PRINT)
.putExtra("id", data.message.id));
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void onMenuShowHeaders(ActionData data) {
boolean show_headers = !properties.getValue("headers", data.message.id);
properties.setValue("headers", data.message.id, show_headers);
if (show_headers && data.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", data.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(context, owner, ex);
}
}.execute(context, owner, args, "message:headers");
} else
notifyDataSetChanged();
}
private void onMenuRaw(ActionData data) {
if (data.message.raw == null) {
Bundle args = new Bundle();
args.putLong("id", data.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(context, owner, ex);
}
}.execute(context, owner, args, "message:raw");
} else {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_STORE_RAW)
.putExtra("id", data.message.id));
}
}
private void onActionMore(final ActionData data) {
boolean show_headers = properties.getValue("headers", data.message.id);
View anchor = bnvActions.findViewById(R.id.action_more);
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, anchor);
popupMenu.inflate(R.menu.menu_message);
popupMenu.getMenu().findItem(R.id.menu_forward).setEnabled(data.message.content);
popupMenu.getMenu().findItem(R.id.menu_unseen).setEnabled(data.message.uid != null);
popupMenu.getMenu().findItem(R.id.menu_flag_color).setEnabled(data.message.uid != null);
popupMenu.getMenu().findItem(R.id.menu_copy).setEnabled(data.message.uid != null);
popupMenu.getMenu().findItem(R.id.menu_delete).setVisible(debug);
popupMenu.getMenu().findItem(R.id.menu_junk).setEnabled(data.message.uid != null);
popupMenu.getMenu().findItem(R.id.menu_junk).setVisible(
data.hasJunk && !EntityFolder.JUNK.equals(data.message.folderType));
popupMenu.getMenu().findItem(R.id.menu_share).setEnabled(data.message.content);
popupMenu.getMenu().findItem(R.id.menu_print).setEnabled(hasWebView && data.message.content);
popupMenu.getMenu().findItem(R.id.menu_print).setVisible(Helper.canPrint(context));
popupMenu.getMenu().findItem(R.id.menu_show_headers).setChecked(show_headers);
popupMenu.getMenu().findItem(R.id.menu_show_headers).setEnabled(data.message.uid != null);
popupMenu.getMenu().findItem(R.id.menu_raw).setEnabled(
data.message.uid != null && (data.message.raw == null || data.message.raw));
popupMenu.getMenu().findItem(R.id.menu_raw).setTitle(
data.message.raw == null || !data.message.raw ? R.string.title_raw_download : R.string.title_raw_save);
popupMenu.getMenu().findItem(R.id.menu_manage_keywords).setEnabled(data.message.uid != null);
popupMenu.getMenu().findItem(R.id.menu_decrypt).setEnabled(
data.message.content && data.message.to != null && data.message.to.length > 0);
popupMenu.getMenu().findItem(R.id.menu_resync).setEnabled(data.message.uid != null);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem target) {
switch (target.getItemId()) {
case R.id.menu_forward:
onMenuForward(data);
return true;
case R.id.menu_unseen:
onMenuUnseen(data);
return true;
case R.id.menu_flag_color:
onMenuColoredStar(data);
return true;
case R.id.menu_copy:
onMenuCopy(data);
return true;
case R.id.menu_delete:
// For emergencies
onMenuDelete(data);
return true;
case R.id.menu_junk:
onMenuJunk(data);
return true;
case R.id.menu_decrypt:
onMenuDecrypt(data);
return true;
case R.id.menu_resync:
onMenuResync(data);
return true;
case R.id.menu_create_rule:
onMenuCreateRule(data);
return true;
case R.id.menu_manage_keywords:
onMenuManageKeywords(data);
return true;
case R.id.menu_share:
onMenuShare(data);
return true;
case R.id.menu_print:
onMenuPrint(data);
return true;
case R.id.menu_show_headers:
onMenuShowHeaders(data);
return true;
case R.id.menu_raw:
onMenuRaw(data);
return true;
default:
return false;
}
}
});
popupMenu.show();
}
private void onActionDelete(final ActionData data) {
if (data.delete) {
// No trash or is trash
new DialogBuilderLifecycle(context, owner)
.setMessage(R.string.title_ask_delete)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Bundle args = new Bundle();
args.putLong("id", data.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;
EntityFolder folder = db.folder().getFolder(message.folder);
if (EntityFolder.OUTBOX.equals(folder.type)) {
db.message().deleteMessage(id);
db.folder().setFolderError(message.folder, null);
db.identity().setIdentityError(message.identity, null);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel("send", message.identity.intValue());
} else
EntityOperation.queue(context, message, EntityOperation.DELETE);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:delete");
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
} else
properties.move(data.message.id, EntityFolder.TRASH, true);
}
private void onActionMove(ActionData data) {
Bundle args = new Bundle();
args.putLong("id", data.message.id);
new SimpleTask>() {
@Override
protected List onExecute(Context context, Bundle args) {
EntityMessage message;
List folders = null;
DB db = DB.getInstance(context);
try {
db.beginTransaction();
message = db.message().getMessage(args.getLong("id"));
if (message == null)
return null;
EntityFolder folder = db.folder().getFolder(message.folder);
if (EntityFolder.OUTBOX.equals(folder.type)) {
long id = message.id;
File source = message.getFile(context);
// Insert into drafts
EntityFolder drafts = db.folder().getFolderByType(message.account, EntityFolder.DRAFTS);
message.id = null;
message.folder = drafts.id;
message.ui_snoozed = null;
message.id = db.message().insertMessage(message);
File target = message.getFile(context);
source.renameTo(target);
List attachments = db.attachment().getAttachments(id);
for (EntityAttachment attachment : attachments)
db.attachment().setMessage(attachment.id, message.id);
EntityOperation.queue(context, message, EntityOperation.ADD);
// Delete from outbox
db.message().deleteMessage(id);
} else
folders = db.folder().getFolders(message.account);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (folders == null)
return null;
List targets = new ArrayList<>();
for (EntityFolder folder : folders)
if (!folder.isHidden(context) &&
!folder.id.equals(message.folder) &&
!EntityFolder.ARCHIVE.equals(folder.type) &&
!EntityFolder.TRASH.equals(folder.type) &&
!EntityFolder.JUNK.equals(folder.type))
targets.add(folder);
if (targets.size() > 0)
Collections.sort(targets, targets.get(0).getComparator(context));
return targets;
}
@Override
protected void onExecuted(final Bundle args, List folders) {
if (folders == null)
return;
View anchor = bnvActions.findViewById(R.id.action_move);
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, anchor);
int order = 0;
for (EntityFolder folder : folders)
popupMenu.getMenu().add(Menu.NONE, folder.id.intValue(), order++, folder.getDisplayName(context));
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(final MenuItem target) {
args.putLong("target", target.getItemId());
new SimpleTask() {
@Override
protected String onExecute(Context context, Bundle args) {
long target = args.getLong("target");
return DB.getInstance(context).folder().getFolder(target).name;
}
@Override
protected void onExecuted(Bundle args, String folderName) {
long id = args.getLong("id");
properties.move(id, folderName, false);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:move");
return true;
}
});
popupMenu.show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, args, "message:move:list");
}
private void onActionArchive(ActionData data) {
properties.move(data.message.id, EntityFolder.ARCHIVE, true);
}
private void onActionReplyMenu(final ActionData data) {
View anchor = bnvActions.findViewById(R.id.action_reply);
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, anchor);
popupMenu.inflate(R.menu.menu_reply);
popupMenu.getMenu().findItem(R.id.menu_reply_list).setVisible(data.message.list_post != null);
popupMenu.getMenu().findItem(R.id.menu_reply_receipt).setVisible(data.message.receipt_to != null);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem target) {
switch (target.getItemId()) {
case R.id.menu_reply_to_sender:
onMenuReply(data, "reply");
return true;
case R.id.menu_reply_to_all:
onMenuReply(data, "reply_all");
return true;
case R.id.menu_reply_list:
onMenuReply(data, "list");
return true;
case R.id.menu_reply_receipt:
onMenuReply(data, "receipt");
return true;
case R.id.menu_reply_answer:
onMenuAnswer(data);
return true;
default:
return false;
}
}
});
popupMenu.show();
}
private void onMenuReply(final ActionData data, String action) {
Intent reply = new Intent(context, ActivityCompose.class)
.putExtra("action", action)
.putExtra("reference", data.message.id);
context.startActivity(reply);
}
private void onMenuAnswer(final ActionData data) {
new SimpleTask>() {
@Override
protected List onExecute(Context context, Bundle args) {
return DB.getInstance(context).answer().getAnswers(false);
}
@Override
protected void onExecuted(Bundle args, List answers) {
if (answers == null || answers.size() == 0) {
Snackbar snackbar = Snackbar.make(
view,
context.getString(R.string.title_no_answers),
Snackbar.LENGTH_LONG);
snackbar.setAction(R.string.title_fix, new View.OnClickListener() {
@Override
public void onClick(View v) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(new Intent(ActivityView.ACTION_EDIT_ANSWERS));
}
});
snackbar.show();
} else {
View anchor = bnvActions.findViewById(R.id.action_reply);
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, anchor);
int order = 0;
for (EntityAnswer answer : answers)
popupMenu.getMenu().add(Menu.NONE, answer.id.intValue(), order++, answer.name);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem target) {
if (Helper.isPro(context))
context.startActivity(new Intent(context, ActivityCompose.class)
.putExtra("action", "reply")
.putExtra("reference", data.message.id)
.putExtra("answer", (long) target.getItemId()));
else {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(new Intent(ActivityView.ACTION_SHOW_PRO));
}
return true;
}
});
popupMenu.show();
}
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(context, owner, ex);
}
}.execute(context, owner, new Bundle(), "message:answer");
}
ItemDetailsLookup.ItemDetails getItemDetails(@NonNull MotionEvent motionEvent) {
return new ItemDetailsMessage(this);
}
Long getKey() {
return getKeyAtPosition(getAdapterPosition());
}
private class OriginalMessage {
String html;
boolean has_images;
}
private class ActionData {
boolean hasJunk;
boolean delete;
TupleMessageEx message;
}
}
AdapterMessage(Context context, LifecycleOwner owner,
ViewType viewType, boolean compact, int zoom, String sort, boolean filter_duplicates, final IProperties properties) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
this.TF = Helper.getTimeInstance(context, SimpleDateFormat.SHORT);
this.context = context;
this.owner = owner;
this.inflater = LayoutInflater.from(context);
this.viewType = viewType;
this.compact = compact;
this.zoom = zoom;
this.sort = sort;
this.filter_duplicates = filter_duplicates;
this.suitable = ConnectionHelper.getNetworkState(context).isSuitable();
this.properties = properties;
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.textColorSecondary = Helper.resolveColor(context, android.R.attr.textColorSecondary);
this.colorUnread = Helper.resolveColor(context, R.attr.colorUnread);
this.dark = Helper.isDarkTheme(context);
this.hasWebView = Helper.hasWebView(context);
this.contacts = Helper.hasPermission(context, Manifest.permission.READ_CONTACTS);
this.textSize = Helper.getTextSize(context, zoom);
this.date = prefs.getBoolean("date", true);
this.threading = prefs.getBoolean("threading", true);
this.avatars = (prefs.getBoolean("avatars", true) ||
prefs.getBoolean("identicons", false));
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.attachments_alt = prefs.getBoolean("attachments_alt", false);
this.monospaced = prefs.getBoolean("monospaced", false);
this.autohtml = (this.hasWebView && this.contacts && prefs.getBoolean("autohtml", false));
this.autoimages = (this.contacts && prefs.getBoolean("autoimages", false));
this.authentication = prefs.getBoolean("authentication", false);
this.debug = prefs.getBoolean("debug", false);
this.differ = new AsyncPagedListDiffer<>(this, DIFF_CALLBACK);
this.differ.addPagedListListener(new AsyncPagedListDiffer.PagedListListener() {
@Override
public void onCurrentListChanged(@Nullable PagedList previousList, @Nullable PagedList currentList) {
if (gotoTop) {
gotoTop = false;
properties.scrollTo(0);
}
}
});
}
void gotoTop() {
properties.scrollTo(0);
this.gotoTop = true;
}
void submitList(PagedList list) {
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();
}
}
@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) {
return prev.uiEquals(next);
}
};
@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)
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) {
PagedList messages = getCurrentList();
if (messages != null)
for (int i = 0; i < messages.size(); i++) {
TupleMessageEx message = messages.get(i);
if (message != null && message.id.equals(key)) {
Log.i("Position=" + i + " @Key=" + key);
return i;
}
}
Log.i("Position=" + RecyclerView.NO_POSITION + " @Key=" + key);
return RecyclerView.NO_POSITION;
}
TupleMessageEx getItemAtPosition(int pos) {
PagedList list = getCurrentList();
if (list != null && pos >= 0 && pos < list.size()) {
TupleMessageEx message = list.get(pos);
Long key = (message == null ? null : message.id);
Log.i("Item=" + key + " @Position=" + pos);
return message;
} else {
Log.i("Item=" + null + " @Position=" + pos);
return null;
}
}
TupleMessageEx getItemForKey(long key) {
PagedList messages = getCurrentList();
if (messages != null)
for (int i = 0; i < messages.size(); i++) {
TupleMessageEx message = messages.get(i);
if (message != null && message.id.equals(key)) {
Log.i("Item=" + message.id + " @Key=" + key);
return message;
}
}
Log.i("Item=" + null + " @Key=" + key);
return null;
}
Long getKeyAtPosition(int pos) {
TupleMessageEx message = getItemAtPosition(pos);
Long key = (message == null ? null : message.id);
Log.i("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 setHtml(long id, String html);
String getHtml(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 target, boolean type);
void finish();
}
}