Simple email application for Android. Original source code: https://framagit.org/dystopia-project/simple-email

420 lines
18 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. package eu.faircode.email;
  2. /*
  3. This file is part of FairEmail.
  4. FairEmail is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. NetGuard is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
  14. Copyright 2018 by Marcel Bokhorst (M66B)
  15. */
  16. import android.Manifest;
  17. import android.content.ContentResolver;
  18. import android.content.Context;
  19. import android.content.DialogInterface;
  20. import android.content.Intent;
  21. import android.content.SharedPreferences;
  22. import android.content.pm.PackageManager;
  23. import android.graphics.Color;
  24. import android.graphics.Typeface;
  25. import android.graphics.drawable.Drawable;
  26. import android.net.Uri;
  27. import android.os.Bundle;
  28. import android.preference.PreferenceManager;
  29. import android.provider.ContactsContract;
  30. import android.text.format.DateUtils;
  31. import android.view.LayoutInflater;
  32. import android.view.Menu;
  33. import android.view.MenuItem;
  34. import android.view.View;
  35. import android.view.ViewGroup;
  36. import android.widget.ImageView;
  37. import android.widget.ProgressBar;
  38. import android.widget.TextView;
  39. import java.io.InputStream;
  40. import java.text.DateFormat;
  41. import java.text.SimpleDateFormat;
  42. import java.util.Date;
  43. import java.util.List;
  44. import java.util.concurrent.ExecutorService;
  45. import java.util.concurrent.Executors;
  46. import androidx.annotation.NonNull;
  47. import androidx.appcompat.widget.PopupMenu;
  48. import androidx.core.content.ContextCompat;
  49. import androidx.lifecycle.LifecycleOwner;
  50. import androidx.lifecycle.Observer;
  51. import androidx.localbroadcastmanager.content.LocalBroadcastManager;
  52. import androidx.paging.PagedListAdapter;
  53. import androidx.recyclerview.widget.DiffUtil;
  54. import androidx.recyclerview.widget.RecyclerView;
  55. public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMessage.ViewHolder> {
  56. private Context context;
  57. private LifecycleOwner owner;
  58. private ViewType viewType;
  59. private boolean avatars;
  60. private boolean debug;
  61. private ExecutorService executor = Executors.newCachedThreadPool(Helper.backgroundThreadFactory);
  62. private DateFormat df = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.LONG);
  63. enum ViewType {UNIFIED, FOLDER, THREAD, SEARCH}
  64. public class ViewHolder extends RecyclerView.ViewHolder
  65. implements View.OnClickListener, View.OnLongClickListener {
  66. View itemView;
  67. View vwColor;
  68. ImageView ivAvatar;
  69. ImageView ivFlagged;
  70. TextView tvFrom;
  71. TextView tvSize;
  72. TextView tvTime;
  73. ImageView ivAttachments;
  74. TextView tvSubject;
  75. TextView tvFolder;
  76. TextView tvCount;
  77. ImageView ivThread;
  78. TextView tvError;
  79. ProgressBar pbLoading;
  80. private static final int action_flag = 1;
  81. private static final int action_seen = 2;
  82. private static final int action_delete = 3;
  83. ViewHolder(View itemView) {
  84. super(itemView);
  85. this.itemView = itemView.findViewById(R.id.clItem);
  86. vwColor = itemView.findViewById(R.id.vwColor);
  87. ivFlagged = itemView.findViewById(R.id.ivFlagged);
  88. ivAvatar = itemView.findViewById(R.id.ivAvatar);
  89. tvFrom = itemView.findViewById(R.id.tvFrom);
  90. tvSize = itemView.findViewById(R.id.tvSize);
  91. tvTime = itemView.findViewById(R.id.tvTime);
  92. ivAttachments = itemView.findViewById(R.id.ivAttachments);
  93. tvSubject = itemView.findViewById(R.id.tvSubject);
  94. tvFolder = itemView.findViewById(R.id.tvFolder);
  95. tvCount = itemView.findViewById(R.id.tvCount);
  96. ivThread = itemView.findViewById(R.id.ivThread);
  97. tvError = itemView.findViewById(R.id.tvError);
  98. pbLoading = itemView.findViewById(R.id.pbLoading);
  99. }
  100. private void wire() {
  101. itemView.setOnClickListener(this);
  102. itemView.setOnLongClickListener(this);
  103. }
  104. private void unwire() {
  105. itemView.setOnClickListener(null);
  106. itemView.setOnLongClickListener(null);
  107. }
  108. private void clear() {
  109. vwColor.setBackgroundColor(Color.TRANSPARENT);
  110. ivFlagged.setVisibility(View.GONE);
  111. ivAvatar.setVisibility(View.GONE);
  112. tvFrom.setText(null);
  113. tvSize.setText(null);
  114. tvTime.setText(null);
  115. ivAttachments.setVisibility(View.GONE);
  116. tvSubject.setText(null);
  117. tvFolder.setText(null);
  118. tvCount.setText(null);
  119. ivThread.setVisibility(View.GONE);
  120. tvError.setVisibility(View.GONE);
  121. pbLoading.setVisibility(View.VISIBLE);
  122. }
  123. private void bindTo(final TupleMessageEx message) {
  124. pbLoading.setVisibility(View.GONE);
  125. itemView.setAlpha(viewType == ViewType.THREAD && EntityFolder.ARCHIVE.equals(message.folderType) ? 0.5f : 1.0f);
  126. boolean photo = false;
  127. if (avatars && message.avatar != null) {
  128. ContentResolver resolver = context.getContentResolver();
  129. InputStream is = ContactsContract.Contacts.openContactPhotoInputStream(resolver, Uri.parse(message.avatar));
  130. if (is != null) {
  131. photo = true;
  132. ivAvatar.setImageDrawable(Drawable.createFromStream(is, "avatar"));
  133. }
  134. }
  135. ivAvatar.setVisibility(photo ? View.VISIBLE : View.GONE);
  136. vwColor.setBackgroundColor(message.accountColor == null ? Color.TRANSPARENT : message.accountColor);
  137. vwColor.setVisibility(viewType == ViewType.UNIFIED && message.accountColor != null ? View.VISIBLE : View.GONE);
  138. if (viewType == ViewType.THREAD)
  139. ivFlagged.setVisibility(message.unflagged == 1 ? View.GONE : View.VISIBLE);
  140. else
  141. ivFlagged.setVisibility(message.count - message.unflagged > 0 ? View.VISIBLE : View.GONE);
  142. if (EntityFolder.DRAFTS.equals(message.folderType) ||
  143. EntityFolder.OUTBOX.equals(message.folderType) ||
  144. EntityFolder.SENT.equals(message.folderType)) {
  145. tvFrom.setText(MessageHelper.getFormattedAddresses(message.to, false));
  146. tvTime.setText(DateUtils.getRelativeTimeSpanString(context, message.sent == null ? message.received : message.sent));
  147. } else {
  148. tvFrom.setText(MessageHelper.getFormattedAddresses(message.from, false));
  149. tvTime.setText(DateUtils.getRelativeTimeSpanString(context, message.received));
  150. }
  151. tvSize.setText(message.size == null ? null : Helper.humanReadableByteCount(message.size, true));
  152. tvSize.setTypeface(null, message.content ? Typeface.NORMAL : Typeface.BOLD);
  153. tvSize.setVisibility(message.size == null ? View.GONE : View.VISIBLE);
  154. ivAttachments.setVisibility(message.attachments > 0 ? View.VISIBLE : View.GONE);
  155. tvSubject.setText(message.subject);
  156. if (viewType == ViewType.UNIFIED)
  157. tvFolder.setText(message.accountName);
  158. else if (viewType == ViewType.FOLDER)
  159. tvFolder.setVisibility(View.GONE);
  160. else {
  161. String name = (message.folderDisplay == null
  162. ? Helper.localizeFolderName(context, message.folderName)
  163. : message.folderDisplay);
  164. tvFolder.setText(name);
  165. }
  166. if (viewType == ViewType.THREAD) {
  167. tvCount.setVisibility(View.GONE);
  168. ivThread.setVisibility(View.GONE);
  169. } else {
  170. tvCount.setText(Integer.toString(message.count));
  171. ivThread.setVisibility(View.VISIBLE);
  172. tvCount.setAlpha(message.threaded ? 1.0f : 0.5f);
  173. ivThread.setAlpha(message.threaded ? 1.0f : 0.5f);
  174. }
  175. if (debug) {
  176. DB db = DB.getInstance(context);
  177. db.operation().getOperationsByMessage(message.id).removeObservers(owner);
  178. db.operation().getOperationsByMessage(message.id).observe(owner, new Observer<List<EntityOperation>>() {
  179. @Override
  180. public void onChanged(List<EntityOperation> operations) {
  181. String text = message.error +
  182. "\n" + message.id + " " + df.format(new Date(message.received)) +
  183. "\n" + (message.ui_hide ? "HIDDEN " : "") +
  184. "seen=" + message.seen + "/" + message.ui_seen + "/" + message.unseen +
  185. " " + message.uid + "/" + message.id +
  186. "\n" + message.msgid;
  187. if (operations != null)
  188. for (EntityOperation op : operations)
  189. text += "\n" + op.id + ":" + op.name + " " + df.format(new Date(op.created));
  190. tvError.setText(text);
  191. tvError.setVisibility(View.VISIBLE);
  192. }
  193. });
  194. }
  195. tvError.setText(message.error);
  196. tvError.setVisibility(message.error == null ? View.GONE : View.VISIBLE);
  197. int typeface = (message.unseen > 0 ? Typeface.BOLD : Typeface.NORMAL);
  198. tvFrom.setTypeface(null, typeface);
  199. tvTime.setTypeface(null, typeface);
  200. tvSubject.setTypeface(null, typeface);
  201. tvCount.setTypeface(null, typeface);
  202. int colorUnseen = Helper.resolveColor(context, message.unseen > 0
  203. ? R.attr.colorUnread : android.R.attr.textColorSecondary);
  204. tvFrom.setTextColor(colorUnseen);
  205. tvTime.setTextColor(colorUnseen);
  206. }
  207. @Override
  208. public void onClick(View view) {
  209. int pos = getAdapterPosition();
  210. if (pos == RecyclerView.NO_POSITION)
  211. return;
  212. TupleMessageEx message = getItem(pos);
  213. if (EntityFolder.DRAFTS.equals(message.folderType))
  214. context.startActivity(
  215. new Intent(context, ActivityCompose.class)
  216. .putExtra("action", "edit")
  217. .putExtra("id", message.id));
  218. else {
  219. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
  220. lbm.sendBroadcast(
  221. new Intent(ActivityView.ACTION_VIEW_MESSAGE)
  222. .putExtra("message", message));
  223. }
  224. }
  225. @Override
  226. public boolean onLongClick(View view) {
  227. int pos = getAdapterPosition();
  228. if (pos == RecyclerView.NO_POSITION)
  229. return false;
  230. final TupleMessageEx message = getItem(pos);
  231. PopupMenu popupMenu = new PopupMenu(context, itemView);
  232. if (!message.threaded && !EntityFolder.OUTBOX.equals(message.folderType)) {
  233. popupMenu.getMenu().add(Menu.NONE, action_flag, 1, message.ui_flagged ? R.string.title_unflag : R.string.title_flag);
  234. popupMenu.getMenu().add(Menu.NONE, action_seen, 2, message.ui_seen ? R.string.title_unseen : R.string.title_seen);
  235. }
  236. if (EntityFolder.TRASH.equals(message.folderType))
  237. popupMenu.getMenu().add(Menu.NONE, action_delete, 3, R.string.title_delete);
  238. popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
  239. @Override
  240. public boolean onMenuItemClick(MenuItem target) {
  241. Bundle args = new Bundle();
  242. args.putLong("id", message.id);
  243. args.putInt("action", target.getItemId());
  244. if (target.getItemId() == action_delete) {
  245. new DialogBuilderLifecycle(context, owner)
  246. .setMessage(R.string.title_ask_delete)
  247. .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
  248. @Override
  249. public void onClick(DialogInterface dialog, int which) {
  250. Bundle args = new Bundle();
  251. args.putLong("id", message.id);
  252. new SimpleTask<Void>() {
  253. @Override
  254. protected Void onLoad(Context context, Bundle args) {
  255. long id = args.getLong("id");
  256. DB db = DB.getInstance(context);
  257. try {
  258. db.beginTransaction();
  259. EntityMessage message = db.message().getMessage(id);
  260. db.message().setMessageUiHide(id, true);
  261. EntityOperation.queue(db, message, EntityOperation.DELETE);
  262. db.setTransactionSuccessful();
  263. } finally {
  264. db.endTransaction();
  265. }
  266. EntityOperation.process(context);
  267. return null;
  268. }
  269. @Override
  270. protected void onException(Bundle args, Throwable ex) {
  271. Helper.unexpectedError(context, ex);
  272. }
  273. }.load(context, owner, args);
  274. }
  275. })
  276. .setNegativeButton(android.R.string.cancel, null)
  277. .show();
  278. } else
  279. new SimpleTask<Void>() {
  280. @Override
  281. protected Void onLoad(final Context context, Bundle args) {
  282. long id = args.getLong("id");
  283. int action = args.getInt("action");
  284. DB db = DB.getInstance(context);
  285. try {
  286. db.beginTransaction();
  287. EntityMessage message = db.message().getMessage(id);
  288. if (action == action_flag) {
  289. db.message().setMessageUiFlagged(message.id, !message.ui_flagged);
  290. EntityOperation.queue(db, message, EntityOperation.FLAG, !message.ui_flagged);
  291. } else if (action == action_seen) {
  292. db.message().setMessageUiSeen(message.id, !message.ui_seen);
  293. EntityOperation.queue(db, message, EntityOperation.SEEN, !message.ui_seen);
  294. }
  295. db.setTransactionSuccessful();
  296. } finally {
  297. db.endTransaction();
  298. }
  299. EntityOperation.process(context);
  300. return null;
  301. }
  302. @Override
  303. public void onException(Bundle args, Throwable ex) {
  304. Helper.unexpectedError(context, ex);
  305. }
  306. }.load(context, owner, args);
  307. return true;
  308. }
  309. });
  310. if (popupMenu.getMenu().hasVisibleItems())
  311. popupMenu.show();
  312. return true;
  313. }
  314. }
  315. AdapterMessage(Context context, LifecycleOwner owner, ViewType viewType) {
  316. super(DIFF_CALLBACK);
  317. this.context = context;
  318. this.owner = owner;
  319. this.viewType = viewType;
  320. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
  321. this.avatars = (prefs.getBoolean("avatars", true) &&
  322. ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
  323. == PackageManager.PERMISSION_GRANTED);
  324. this.debug = prefs.getBoolean("debug", false);
  325. }
  326. private static final DiffUtil.ItemCallback<TupleMessageEx> DIFF_CALLBACK =
  327. new DiffUtil.ItemCallback<TupleMessageEx>() {
  328. @Override
  329. public boolean areItemsTheSame(
  330. @NonNull TupleMessageEx prev, @NonNull TupleMessageEx next) {
  331. return prev.id.equals(next.id);
  332. }
  333. @Override
  334. public boolean areContentsTheSame(
  335. @NonNull TupleMessageEx prev, @NonNull TupleMessageEx next) {
  336. return prev.equals(next);
  337. }
  338. };
  339. @Override
  340. @NonNull
  341. public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
  342. return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_message, parent, false));
  343. }
  344. @Override
  345. public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
  346. holder.unwire();
  347. TupleMessageEx message = getItem(position);
  348. if (message == null)
  349. holder.clear();
  350. else {
  351. holder.bindTo(message);
  352. holder.wire();
  353. }
  354. }
  355. }