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

841 lines
35 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
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 Safe email.
  4. Safe email 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.Intent;
  18. import android.content.pm.PackageManager;
  19. import android.database.Cursor;
  20. import android.net.Uri;
  21. import android.os.Bundle;
  22. import android.provider.ContactsContract;
  23. import android.provider.OpenableColumns;
  24. import android.text.Html;
  25. import android.text.TextUtils;
  26. import android.util.Log;
  27. import android.view.LayoutInflater;
  28. import android.view.Menu;
  29. import android.view.MenuInflater;
  30. import android.view.MenuItem;
  31. import android.view.View;
  32. import android.view.ViewGroup;
  33. import android.webkit.MimeTypeMap;
  34. import android.widget.ArrayAdapter;
  35. import android.widget.AutoCompleteTextView;
  36. import android.widget.EditText;
  37. import android.widget.FilterQueryProvider;
  38. import android.widget.ImageView;
  39. import android.widget.ProgressBar;
  40. import android.widget.Spinner;
  41. import android.widget.Toast;
  42. import com.google.android.material.bottomnavigation.BottomNavigationView;
  43. import com.google.android.material.snackbar.Snackbar;
  44. import java.io.ByteArrayOutputStream;
  45. import java.io.IOException;
  46. import java.io.InputStream;
  47. import java.util.ArrayList;
  48. import java.util.Arrays;
  49. import java.util.Collections;
  50. import java.util.Comparator;
  51. import java.util.Date;
  52. import java.util.List;
  53. import javax.mail.Address;
  54. import javax.mail.MessageRemovedException;
  55. import javax.mail.internet.InternetAddress;
  56. import androidx.annotation.NonNull;
  57. import androidx.annotation.Nullable;
  58. import androidx.constraintlayout.widget.Group;
  59. import androidx.core.content.ContextCompat;
  60. import androidx.cursoradapter.widget.SimpleCursorAdapter;
  61. import androidx.fragment.app.FragmentTransaction;
  62. import androidx.lifecycle.Observer;
  63. import androidx.recyclerview.widget.LinearLayoutManager;
  64. import androidx.recyclerview.widget.RecyclerView;
  65. import static android.app.Activity.RESULT_OK;
  66. public class FragmentCompose extends FragmentEx {
  67. private ViewGroup view;
  68. private Spinner spFrom;
  69. private ImageView ivIdentityAdd;
  70. private AutoCompleteTextView etTo;
  71. private ImageView ivToAdd;
  72. private AutoCompleteTextView etCc;
  73. private ImageView ivCcAdd;
  74. private AutoCompleteTextView etBcc;
  75. private ImageView ivBccAdd;
  76. private EditText etSubject;
  77. private RecyclerView rvAttachment;
  78. private EditText etBody;
  79. private BottomNavigationView bottom_navigation;
  80. private ProgressBar pbWait;
  81. private Group grpAddresses;
  82. private Group grpAttachments;
  83. private Group grpReady;
  84. private AdapterAttachment adapter;
  85. private boolean attaching = false;
  86. private String action = null;
  87. private long id = -1; // draft id
  88. private long account = -1;
  89. private long reference = -1;
  90. private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes
  91. @Override
  92. public void onSaveInstanceState(Bundle outState) {
  93. Log.i(Helper.TAG, "Saving state");
  94. outState.putString("action", action);
  95. outState.putLong("id", id);
  96. outState.putLong("account", account);
  97. outState.putLong("reference", reference);
  98. super.onSaveInstanceState(outState);
  99. }
  100. @Override
  101. @Nullable
  102. public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
  103. // Get arguments
  104. if (savedInstanceState == null) {
  105. if (action == null) {
  106. action = getArguments().getString("action");
  107. id = getArguments().getLong("id", -1);
  108. account = getArguments().getLong("account", -1);
  109. reference = getArguments().getLong("reference", -1);
  110. }
  111. } else {
  112. Log.i(Helper.TAG, "Restoring state");
  113. action = savedInstanceState.getString("action");
  114. id = savedInstanceState.getLong("id", -1);
  115. account = savedInstanceState.getLong("account", -1);
  116. reference = savedInstanceState.getLong("reference", -1);
  117. }
  118. setSubtitle(R.string.title_compose);
  119. view = (ViewGroup) inflater.inflate(R.layout.fragment_compose, container, false);
  120. // Get controls
  121. spFrom = view.findViewById(R.id.spFrom);
  122. ivIdentityAdd = view.findViewById(R.id.ivIdentityAdd);
  123. etTo = view.findViewById(R.id.etTo);
  124. ivToAdd = view.findViewById(R.id.ivToAdd);
  125. etCc = view.findViewById(R.id.etCc);
  126. ivCcAdd = view.findViewById(R.id.ivCcAdd);
  127. etBcc = view.findViewById(R.id.etBcc);
  128. ivBccAdd = view.findViewById(R.id.ivBccAdd);
  129. etSubject = view.findViewById(R.id.etSubject);
  130. rvAttachment = view.findViewById(R.id.rvAttachment);
  131. etBody = view.findViewById(R.id.etBody);
  132. bottom_navigation = view.findViewById(R.id.bottom_navigation);
  133. pbWait = view.findViewById(R.id.pbWait);
  134. grpAddresses = view.findViewById(R.id.grpAddresses);
  135. grpAttachments = view.findViewById(R.id.grpAttachments);
  136. grpReady = view.findViewById(R.id.grpReady);
  137. // Wire controls
  138. ivIdentityAdd.setOnClickListener(new View.OnClickListener() {
  139. @Override
  140. public void onClick(View view) {
  141. Bundle args = new Bundle();
  142. args.putLong("id", -1);
  143. FragmentIdentity fragment = new FragmentIdentity();
  144. fragment.setArguments(args);
  145. FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
  146. fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("identity");
  147. fragmentTransaction.commit();
  148. }
  149. });
  150. ivToAdd.setOnClickListener(new View.OnClickListener() {
  151. @Override
  152. public void onClick(View view) {
  153. Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
  154. startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT_TO);
  155. }
  156. });
  157. ivCcAdd.setOnClickListener(new View.OnClickListener() {
  158. @Override
  159. public void onClick(View view) {
  160. Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
  161. startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT_CC);
  162. }
  163. });
  164. ivBccAdd.setOnClickListener(new View.OnClickListener() {
  165. @Override
  166. public void onClick(View view) {
  167. Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI);
  168. startActivityForResult(intent, ActivityCompose.REQUEST_CONTACT_BCC);
  169. }
  170. });
  171. bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
  172. @Override
  173. public boolean onNavigationItemSelected(@NonNull MenuItem item) {
  174. onAction(item.getItemId());
  175. return false;
  176. }
  177. });
  178. setHasOptionsMenu(true);
  179. // Initialize
  180. spFrom.setVisibility(View.GONE);
  181. ivIdentityAdd.setVisibility(View.GONE);
  182. grpAddresses.setVisibility(View.GONE);
  183. grpAttachments.setVisibility(View.GONE);
  184. grpReady.setVisibility(View.GONE);
  185. pbWait.setVisibility(View.VISIBLE);
  186. bottom_navigation.getMenu().setGroupEnabled(0, false);
  187. if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_CONTACTS)
  188. == PackageManager.PERMISSION_GRANTED) {
  189. SimpleCursorAdapter adapter = new SimpleCursorAdapter(
  190. getContext(),
  191. android.R.layout.simple_list_item_2,
  192. null,
  193. new String[]{
  194. ContactsContract.Contacts.DISPLAY_NAME,
  195. ContactsContract.CommonDataKinds.Email.DATA
  196. },
  197. new int[]{
  198. android.R.id.text1,
  199. android.R.id.text2
  200. },
  201. 0);
  202. etTo.setAdapter(adapter);
  203. etCc.setAdapter(adapter);
  204. etBcc.setAdapter(adapter);
  205. adapter.setFilterQueryProvider(new FilterQueryProvider() {
  206. public Cursor runQuery(CharSequence typed) {
  207. return getContext().getContentResolver().query(
  208. ContactsContract.CommonDataKinds.Email.CONTENT_URI,
  209. new String[]{
  210. ContactsContract.RawContacts._ID,
  211. ContactsContract.Contacts.DISPLAY_NAME,
  212. ContactsContract.CommonDataKinds.Email.DATA
  213. },
  214. ContactsContract.CommonDataKinds.Email.DATA + " <> ''" +
  215. " AND (" + ContactsContract.Contacts.DISPLAY_NAME + " LIKE '%" + typed + "%'" +
  216. " OR " + ContactsContract.CommonDataKinds.Email.DATA + " LIKE '%" + typed + "%')",
  217. null,
  218. "CASE WHEN " + ContactsContract.Contacts.DISPLAY_NAME + " NOT LIKE '%@%' THEN 0 ELSE 1 END" +
  219. ", " + ContactsContract.Contacts.DISPLAY_NAME +
  220. ", " + ContactsContract.CommonDataKinds.Email.DATA + " COLLATE NOCASE");
  221. }
  222. });
  223. adapter.setCursorToStringConverter(new SimpleCursorAdapter.CursorToStringConverter() {
  224. public CharSequence convertToString(Cursor cursor) {
  225. int colName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
  226. int colEmail = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
  227. return cursor.getString(colName) + "<" + cursor.getString(colEmail) + ">";
  228. }
  229. });
  230. }
  231. rvAttachment.setHasFixedSize(false);
  232. LinearLayoutManager llm = new LinearLayoutManager(getContext());
  233. rvAttachment.setLayoutManager(llm);
  234. adapter = new AdapterAttachment(getContext());
  235. rvAttachment.setAdapter(adapter);
  236. return view;
  237. }
  238. @Override
  239. public void onActivityCreated(@Nullable Bundle savedInstanceState) {
  240. super.onActivityCreated(savedInstanceState);
  241. DB.getInstance(getContext()).identity().liveIdentities(true).observe(getViewLifecycleOwner(), new Observer<List<EntityIdentity>>() {
  242. @Override
  243. public void onChanged(@Nullable final List<EntityIdentity> identities) {
  244. Log.i(Helper.TAG, "Set identities=" + identities.size());
  245. // Sort identities
  246. Collections.sort(identities, new Comparator<EntityIdentity>() {
  247. @Override
  248. public int compare(EntityIdentity i1, EntityIdentity i2) {
  249. return i1.name.compareTo(i2.name);
  250. }
  251. });
  252. // Show identities
  253. ArrayAdapter<EntityIdentity> adapter = new ArrayAdapter<>(getContext(), R.layout.spinner_item, identities);
  254. adapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
  255. spFrom.setAdapter(adapter);
  256. // Select primary identity
  257. for (int pos = 0; pos < identities.size(); pos++)
  258. if (identities.get(pos).primary) {
  259. spFrom.setSelection(pos);
  260. break;
  261. }
  262. spFrom.setVisibility(View.VISIBLE);
  263. ivIdentityAdd.setVisibility(View.VISIBLE);
  264. // Get draft, might select another identity
  265. Bundle args = new Bundle();
  266. args.putString("action", action);
  267. args.putLong("id", id);
  268. args.putLong("account", account);
  269. args.putLong("reference", reference);
  270. getLoader.load(FragmentCompose.this, ActivityCompose.LOADER_COMPOSE_GET, args);
  271. }
  272. });
  273. }
  274. @Override
  275. public void onPause() {
  276. if (!attaching)
  277. onAction(R.id.action_save);
  278. super.onPause();
  279. }
  280. @Override
  281. public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
  282. inflater.inflate(R.menu.menu_compose, menu);
  283. super.onCreateOptionsMenu(menu, inflater);
  284. }
  285. @Override
  286. public void onPrepareOptionsMenu(Menu menu) {
  287. super.onPrepareOptionsMenu(menu);
  288. menu.findItem(R.id.menu_attachment).setEnabled(id >= 0);
  289. }
  290. @Override
  291. public boolean onOptionsItemSelected(MenuItem item) {
  292. switch (item.getItemId()) {
  293. case R.id.menu_attachment:
  294. onMenuAttachment();
  295. return true;
  296. case R.id.menu_addresses:
  297. onMenuAddresses();
  298. return true;
  299. default:
  300. return super.onOptionsItemSelected(item);
  301. }
  302. }
  303. private void onMenuAttachment() {
  304. attaching = true;
  305. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
  306. intent.addCategory(Intent.CATEGORY_OPENABLE);
  307. intent.setType("*/*");
  308. startActivityForResult(intent, ActivityCompose.REQUEST_ATTACHMENT);
  309. }
  310. private void onMenuAddresses() {
  311. grpAddresses.setVisibility(grpAddresses.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
  312. }
  313. @Override
  314. public void onActivityResult(int requestCode, int resultCode, Intent data) {
  315. if (resultCode == RESULT_OK) {
  316. if (requestCode == ActivityCompose.REQUEST_ATTACHMENT) {
  317. if (data != null)
  318. handleAddAttachment(data);
  319. } else
  320. handlePickContact(requestCode, data);
  321. }
  322. }
  323. private void handlePickContact(int requestCode, Intent data) {
  324. Cursor cursor = null;
  325. try {
  326. cursor = getContext().getContentResolver().query(data.getData(),
  327. new String[]{
  328. ContactsContract.CommonDataKinds.Email.ADDRESS,
  329. ContactsContract.Contacts.DISPLAY_NAME
  330. },
  331. null, null, null);
  332. if (cursor != null && cursor.moveToFirst()) {
  333. int colEmail = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS);
  334. int colName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
  335. String email = cursor.getString(colEmail);
  336. String name = cursor.getString(colName);
  337. String text = null;
  338. if (requestCode == ActivityCompose.REQUEST_CONTACT_TO)
  339. text = etTo.getText().toString();
  340. else if (requestCode == ActivityCompose.REQUEST_CONTACT_CC)
  341. text = etCc.getText().toString();
  342. else if (requestCode == ActivityCompose.REQUEST_CONTACT_BCC)
  343. text = etBcc.getText().toString();
  344. InternetAddress address = new InternetAddress(email, name);
  345. StringBuilder sb = new StringBuilder(text);
  346. if (sb.length() > 0)
  347. sb.append(", ");
  348. sb.append(address.toString());
  349. if (requestCode == ActivityCompose.REQUEST_CONTACT_TO)
  350. etTo.setText(sb.toString());
  351. else if (requestCode == ActivityCompose.REQUEST_CONTACT_CC)
  352. etCc.setText(sb.toString());
  353. else if (requestCode == ActivityCompose.REQUEST_CONTACT_BCC)
  354. etBcc.setText(sb.toString());
  355. }
  356. } catch (Throwable ex) {
  357. Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  358. Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
  359. } finally {
  360. if (cursor != null)
  361. cursor.close();
  362. }
  363. }
  364. private void handleAddAttachment(Intent data) {
  365. Bundle args = new Bundle();
  366. args.putLong("id", id);
  367. args.putParcelable("uri", data.getData());
  368. new SimpleLoader<Void>() {
  369. @Override
  370. public Void onLoad(Bundle args) throws IOException {
  371. Cursor cursor = null;
  372. try {
  373. Uri uri = args.getParcelable("uri");
  374. cursor = getContext().getContentResolver().query(uri, null, null, null, null, null);
  375. if (cursor == null || !cursor.moveToFirst())
  376. return null;
  377. DB db = DB.getInstance(getContext());
  378. long id = args.getLong("id");
  379. EntityMessage draft = db.message().getMessage(id);
  380. EntityAttachment attachment = new EntityAttachment();
  381. attachment.message = draft.id;
  382. attachment.sequence = db.attachment().getAttachmentCount(draft.id);
  383. attachment.name = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
  384. String extension = MimeTypeMap.getFileExtensionFromUrl(attachment.name.toLowerCase());
  385. if (extension != null)
  386. attachment.type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
  387. if (extension == null)
  388. attachment.type = "application/octet-stream";
  389. String size = cursor.getString(cursor.getColumnIndex(OpenableColumns.SIZE));
  390. attachment.size = (size == null ? null : Integer.parseInt(size));
  391. attachment.progress = 0;
  392. attachment.id = db.attachment().insertAttachment(attachment);
  393. InputStream is = null;
  394. try {
  395. is = getContext().getContentResolver().openInputStream(uri);
  396. ByteArrayOutputStream os = new ByteArrayOutputStream();
  397. int len;
  398. byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE];
  399. while ((len = is.read(buffer)) > 0) {
  400. os.write(buffer, 0, len);
  401. // Update progress
  402. if (attachment.size != null) {
  403. attachment.progress = os.size() * 100 / attachment.size;
  404. db.attachment().updateAttachment(attachment);
  405. }
  406. }
  407. attachment.size = os.size();
  408. attachment.progress = null;
  409. attachment.content = os.toByteArray();
  410. db.attachment().updateAttachment(attachment);
  411. } finally {
  412. if (is != null)
  413. is.close();
  414. }
  415. return null;
  416. } finally {
  417. if (cursor != null)
  418. cursor.close();
  419. }
  420. }
  421. @Override
  422. public void onLoaded(Bundle args, Void data) {
  423. attaching = false;
  424. }
  425. @Override
  426. public void onException(Bundle args, Throwable ex) {
  427. Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
  428. }
  429. }.load(this, ActivityCompose.LOADER_COMPOSE_ATTACHMENT, args);
  430. }
  431. private void onAction(int action) {
  432. bottom_navigation.getMenu().setGroupEnabled(0, false);
  433. EntityIdentity identity = (EntityIdentity) spFrom.getSelectedItem();
  434. Bundle args = new Bundle();
  435. args.putLong("id", id);
  436. args.putInt("action", action);
  437. args.putLong("identity", identity == null ? -1 : identity.id);
  438. args.putString("to", etTo.getText().toString());
  439. args.putString("cc", etCc.getText().toString());
  440. args.putString("bcc", etBcc.getText().toString());
  441. args.putString("subject", etSubject.getText().toString());
  442. args.putString("body", etBody.getText().toString());
  443. putLoader.load(this, ActivityCompose.LOADER_COMPOSE_PUT, args);
  444. }
  445. private SimpleLoader<EntityMessage> getLoader = new SimpleLoader<EntityMessage>() {
  446. @Override
  447. public EntityMessage onLoad(Bundle args) {
  448. String action = args.getString("action");
  449. long id = args.getLong("id", -1);
  450. long account = args.getLong("account", -1);
  451. long reference = args.getLong("reference", -1);
  452. Log.i(Helper.TAG, "Get load action=" + action + " id=" + id + " account=" + account + " reference=" + reference);
  453. DB db = DB.getInstance(getContext());
  454. EntityMessage draft = db.message().getMessage(id);
  455. if (draft == null) {
  456. if ("edit".equals(action))
  457. throw new IllegalStateException("Message to edit not found");
  458. try {
  459. db.beginTransaction();
  460. EntityMessage ref = db.message().getMessage(reference);
  461. if (ref != null)
  462. account = ref.account;
  463. EntityFolder drafts;
  464. drafts = db.folder().getFolderByType(account, EntityFolder.DRAFTS);
  465. if (drafts == null)
  466. drafts = db.folder().getPrimaryDrafts();
  467. draft = new EntityMessage();
  468. draft.account = account;
  469. draft.folder = drafts.id;
  470. if (ref != null) {
  471. draft.thread = ref.thread;
  472. if ("reply".equals(action)) {
  473. draft.replying = ref.id;
  474. draft.to = (ref.reply == null || ref.reply.length == 0 ? ref.from : ref.reply);
  475. draft.from = ref.to;
  476. } else if ("reply_all".equals(action)) {
  477. draft.replying = ref.id;
  478. List<Address> addresses = new ArrayList<>();
  479. if (draft.reply != null && ref.reply.length > 0)
  480. addresses.addAll(Arrays.asList(ref.reply));
  481. else if (draft.from != null)
  482. addresses.addAll(Arrays.asList(ref.from));
  483. if (draft.cc != null)
  484. addresses.addAll(Arrays.asList(ref.cc));
  485. draft.to = addresses.toArray(new Address[0]);
  486. draft.from = ref.to;
  487. } else if ("forward".equals(action)) {
  488. //msg.replying = ref.id;
  489. draft.from = ref.to;
  490. }
  491. if ("reply".equals(action) || "reply_all".equals(action)) {
  492. draft.subject = getContext().getString(R.string.title_subject_reply, ref.subject);
  493. draft.body = String.format("<br><br>%s %s:<br><br>%s",
  494. Html.escapeHtml(new Date().toString()),
  495. Html.escapeHtml(TextUtils.join(", ", draft.to)),
  496. HtmlHelper.sanitize(getContext(), ref.body, true));
  497. } else if ("forward".equals(action)) {
  498. draft.subject = getContext().getString(R.string.title_subject_forward, ref.subject);
  499. draft.body = String.format("<br><br>%s %s:<br><br>%s",
  500. Html.escapeHtml(new Date().toString()),
  501. Html.escapeHtml(TextUtils.join(", ", ref.from)),
  502. HtmlHelper.sanitize(getContext(), ref.body, true));
  503. }
  504. }
  505. if ("new".equals(action))
  506. draft.body = "";
  507. draft.received = new Date().getTime();
  508. draft.seen = false;
  509. draft.ui_seen = false;
  510. draft.ui_hide = false;
  511. draft.id = db.message().insertMessage(draft);
  512. draft.msgid = draft.generateMessageId();
  513. db.message().updateMessage(draft);
  514. args.putLong("id", draft.id);
  515. EntityOperation.queue(db, draft, EntityOperation.ADD);
  516. db.setTransactionSuccessful();
  517. } finally {
  518. db.endTransaction();
  519. }
  520. EntityOperation.process(getContext());
  521. }
  522. return draft;
  523. }
  524. @Override
  525. public void onLoaded(Bundle args, EntityMessage draft) {
  526. id = draft.id;
  527. if ("new".equals(args.getString("action")))
  528. action = "edit";
  529. Log.i(Helper.TAG, "Get loaded action=" + action + " id=" + id);
  530. getActivity().invalidateOptionsMenu();
  531. pbWait.setVisibility(View.GONE);
  532. grpAddresses.setVisibility("reply_all".equals(action) ? View.VISIBLE : View.GONE);
  533. grpReady.setVisibility(View.VISIBLE);
  534. ArrayAdapter aa = (ArrayAdapter) spFrom.getAdapter();
  535. if (aa != null) {
  536. for (int pos = 0; pos < aa.getCount(); pos++) {
  537. EntityIdentity identity = (EntityIdentity) aa.getItem(pos);
  538. if (draft.identity == null
  539. ? draft.from != null && draft.from.length > 0 && ((InternetAddress) draft.from[0]).getAddress().equals(identity.email)
  540. : draft.identity.equals(identity.id)) {
  541. spFrom.setSelection(pos);
  542. break;
  543. }
  544. }
  545. }
  546. etTo.setText(draft.to == null ? null : TextUtils.join(", ", draft.to));
  547. etCc.setText(draft.cc == null ? null : TextUtils.join(", ", draft.cc));
  548. etBcc.setText(draft.bcc == null ? null : TextUtils.join(", ", draft.bcc));
  549. etSubject.setText(draft.subject);
  550. DB db = DB.getInstance(getContext());
  551. db.attachment().liveAttachments(draft.id).removeObservers(getViewLifecycleOwner());
  552. db.attachment().liveAttachments(draft.id).observe(getViewLifecycleOwner(),
  553. new Observer<List<TupleAttachment>>() {
  554. @Override
  555. public void onChanged(@Nullable List<TupleAttachment> attachments) {
  556. adapter.set(attachments);
  557. grpAttachments.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
  558. }
  559. });
  560. etBody.setText(TextUtils.isEmpty(draft.body) ? null : Html.fromHtml(draft.body));
  561. if ("edit".equals(action))
  562. etTo.requestFocus();
  563. else if ("reply".equals(action) || "reply_all".equals(action))
  564. etBody.requestFocus();
  565. else if ("forward".equals(action))
  566. etTo.requestFocus();
  567. bottom_navigation.getMenu().setGroupEnabled(0, true);
  568. }
  569. @Override
  570. public void onException(Bundle args, Throwable ex) {
  571. Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
  572. }
  573. };
  574. private SimpleLoader<EntityMessage> putLoader = new SimpleLoader<EntityMessage>() {
  575. @Override
  576. public EntityMessage onLoad(Bundle args) throws Throwable {
  577. // Get data
  578. long id = args.getLong("id");
  579. int action = args.getInt("action");
  580. long iid = args.getLong("identity");
  581. String to = args.getString("to");
  582. String cc = args.getString("cc");
  583. String bcc = args.getString("bcc");
  584. String subject = args.getString("subject");
  585. String body = args.getString("body");
  586. Log.i(Helper.TAG, "Put load action=" + action + " id=" + id);
  587. // Get draft & selected identity
  588. DB db = DB.getInstance(getContext());
  589. EntityMessage draft = db.message().getMessage(id);
  590. EntityIdentity identity = db.identity().getIdentity(iid);
  591. // Draft deleted by server
  592. // TODO: better handling of remote deleted message
  593. if (draft == null)
  594. throw new MessageRemovedException();
  595. // Convert data
  596. Address afrom[] = (identity == null ? null : new Address[]{new InternetAddress(identity.email, identity.name)});
  597. Address ato[] = (TextUtils.isEmpty(to) ? null : InternetAddress.parse(to));
  598. Address acc[] = (TextUtils.isEmpty(cc) ? null : InternetAddress.parse(cc));
  599. Address abcc[] = (TextUtils.isEmpty(bcc) ? null : InternetAddress.parse(bcc));
  600. // Update draft
  601. draft.identity = (identity == null ? null : identity.id);
  602. draft.from = afrom;
  603. draft.to = ato;
  604. draft.cc = acc;
  605. draft.bcc = abcc;
  606. draft.subject = subject;
  607. draft.body = "<pre>" + body.replaceAll("\\r?\\n", "<br />") + "</pre>";
  608. draft.received = new Date().getTime();
  609. db.message().updateMessage(draft);
  610. // Check data
  611. try {
  612. db.beginTransaction();
  613. if (action == R.id.action_trash) {
  614. draft.ui_hide = true;
  615. db.message().updateMessage(draft);
  616. EntityFolder trash = db.folder().getFolderByType(draft.account, EntityFolder.TRASH);
  617. EntityOperation.queue(db, draft, EntityOperation.MOVE, trash.id);
  618. } else if (action == R.id.action_save) {
  619. String msgid = draft.msgid;
  620. // Save attachments
  621. List<EntityAttachment> attachments = db.attachment().getAttachments(draft.id);
  622. for (EntityAttachment attachment : attachments)
  623. attachment.content = db.attachment().getContent(attachment.id);
  624. // Delete previous draft
  625. draft.msgid = null;
  626. draft.ui_hide = true;
  627. db.message().updateMessage(draft);
  628. EntityOperation.queue(db, draft, EntityOperation.DELETE);
  629. // Create new draft
  630. draft.id = null;
  631. draft.uid = null; // unique index folder/uid
  632. draft.msgid = msgid;
  633. draft.ui_hide = false;
  634. draft.id = db.message().insertMessage(draft);
  635. // Restore attachments
  636. for (EntityAttachment attachment : attachments) {
  637. attachment.message = draft.id;
  638. db.attachment().insertAttachment(attachment);
  639. }
  640. EntityOperation.queue(db, draft, EntityOperation.ADD);
  641. } else if (action == R.id.action_send) {
  642. // Check data
  643. if (draft.identity == null)
  644. throw new IllegalArgumentException(getContext().getString(R.string.title_from_missing));
  645. if (draft.to == null && draft.cc == null && draft.bcc == null)
  646. throw new IllegalArgumentException(getContext().getString(R.string.title_to_missing));
  647. if (db.attachment().getAttachmentCountWithoutContent(draft.id) > 0)
  648. throw new IllegalArgumentException(getContext().getString(R.string.title_attachments_missing));
  649. List<EntityAttachment> attachments = db.attachment().getAttachments(draft.id);
  650. for (EntityAttachment attachment : attachments)
  651. attachment.content = db.attachment().getContent(attachment.id);
  652. String msgid = draft.msgid;
  653. // Delete draft (cannot move to outbox)
  654. draft.msgid = null;
  655. draft.ui_hide = true;
  656. db.message().updateMessage(draft);
  657. EntityOperation.queue(db, draft, EntityOperation.DELETE);
  658. // Copy message to outbox
  659. draft.id = null;
  660. draft.folder = db.folder().getOutbox().id;
  661. draft.uid = null;
  662. draft.msgid = msgid;
  663. draft.ui_hide = false;
  664. draft.id = db.message().insertMessage(draft);
  665. for (EntityAttachment attachment : attachments) {
  666. attachment.message = draft.id;
  667. db.attachment().insertAttachment(attachment);
  668. }
  669. EntityOperation.queue(db, draft, EntityOperation.SEND);
  670. }
  671. db.setTransactionSuccessful();
  672. } finally {
  673. db.endTransaction();
  674. }
  675. EntityOperation.process(getContext());
  676. return draft;
  677. }
  678. @Override
  679. public void onLoaded(Bundle args, EntityMessage draft) {
  680. id = draft.id;
  681. int action = args.getInt("action");
  682. Log.i(Helper.TAG, "Get loaded action=" + action + " id=" + id);
  683. bottom_navigation.getMenu().setGroupEnabled(0, true);
  684. if (action == R.id.action_trash) {
  685. getFragmentManager().popBackStack();
  686. Toast.makeText(getContext(), R.string.title_draft_trashed, Toast.LENGTH_LONG).show();
  687. } else if (action == R.id.action_save)
  688. Toast.makeText(getContext(), R.string.title_draft_saved, Toast.LENGTH_LONG).show();
  689. else if (action == R.id.action_send) {
  690. getFragmentManager().popBackStack();
  691. Toast.makeText(getContext(), R.string.title_queued, Toast.LENGTH_LONG).show();
  692. }
  693. }
  694. @Override
  695. public void onException(Bundle args, Throwable ex) {
  696. bottom_navigation.getMenu().setGroupEnabled(0, true);
  697. if (ex instanceof IllegalArgumentException)
  698. Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
  699. else
  700. Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show();
  701. }
  702. };
  703. }