Simple email application for Android. Original source code: https://framagit.org/dystopia-project/simple-email
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

766 lines
33 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
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.content.Context;
  17. import android.content.Intent;
  18. import android.content.SharedPreferences;
  19. import android.graphics.Canvas;
  20. import android.graphics.drawable.Drawable;
  21. import android.os.Bundle;
  22. import android.os.Handler;
  23. import android.preference.PreferenceManager;
  24. import android.text.TextUtils;
  25. import android.util.Log;
  26. import android.view.LayoutInflater;
  27. import android.view.Menu;
  28. import android.view.MenuInflater;
  29. import android.view.MenuItem;
  30. import android.view.View;
  31. import android.view.ViewGroup;
  32. import android.widget.ImageButton;
  33. import android.widget.ProgressBar;
  34. import android.widget.TextView;
  35. import com.google.android.material.floatingactionbutton.FloatingActionButton;
  36. import com.google.android.material.snackbar.Snackbar;
  37. import java.util.Date;
  38. import java.util.List;
  39. import java.util.concurrent.ExecutorService;
  40. import java.util.concurrent.Executors;
  41. import androidx.annotation.NonNull;
  42. import androidx.annotation.Nullable;
  43. import androidx.appcompat.widget.SearchView;
  44. import androidx.constraintlayout.widget.Group;
  45. import androidx.fragment.app.FragmentTransaction;
  46. import androidx.lifecycle.LiveData;
  47. import androidx.lifecycle.Observer;
  48. import androidx.paging.LivePagedListBuilder;
  49. import androidx.paging.PagedList;
  50. import androidx.recyclerview.widget.ItemTouchHelper;
  51. import androidx.recyclerview.widget.LinearLayoutManager;
  52. import androidx.recyclerview.widget.RecyclerView;
  53. public class FragmentMessages extends FragmentEx {
  54. private ViewGroup view;
  55. private TextView tvSupport;
  56. private ImageButton ibHintSupport;
  57. private ImageButton ibHintActions;
  58. private RecyclerView rvMessage;
  59. private TextView tvNoEmail;
  60. private ProgressBar pbWait;
  61. private Group grpSupport;
  62. private Group grpHintSupport;
  63. private Group grpHintActions;
  64. private Group grpReady;
  65. private FloatingActionButton fab;
  66. private long folder = -1;
  67. private long thread = -1;
  68. private String search = null;
  69. private long primary = -1;
  70. private AdapterMessage adapter;
  71. private AdapterMessage.ViewType viewType;
  72. private LiveData<PagedList<TupleMessageEx>> messages = null;
  73. private SearchState searchState = SearchState.Reset;
  74. private BoundaryCallbackMessages searchCallback = null;
  75. private ExecutorService executor = Executors.newCachedThreadPool(Helper.backgroundThreadFactory);
  76. private static final int MESSAGES_PAGE_SIZE = 50;
  77. private static final int SEARCH_PAGE_SIZE = 10;
  78. private static final int UNDO_TIMEOUT = 5000; // milliseconds
  79. private enum SearchState {Reset, Database, Boundary}
  80. @Override
  81. public void onCreate(Bundle savedInstanceState) {
  82. super.onCreate(savedInstanceState);
  83. // Get arguments
  84. Bundle args = getArguments();
  85. if (args != null) {
  86. folder = args.getLong("folder", -1);
  87. thread = args.getLong("thread", -1); // message ID
  88. search = args.getString("search");
  89. }
  90. }
  91. @Override
  92. @Nullable
  93. public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
  94. view = (ViewGroup) inflater.inflate(R.layout.fragment_messages, container, false);
  95. setHasOptionsMenu(true);
  96. // Get controls
  97. tvSupport = view.findViewById(R.id.tvSupport);
  98. ibHintSupport = view.findViewById(R.id.ibHintSupport);
  99. ibHintActions = view.findViewById(R.id.ibHintActions);
  100. rvMessage = view.findViewById(R.id.rvFolder);
  101. tvNoEmail = view.findViewById(R.id.tvNoEmail);
  102. pbWait = view.findViewById(R.id.pbWait);
  103. grpSupport = view.findViewById(R.id.grpSupport);
  104. grpHintSupport = view.findViewById(R.id.grpHintSupport);
  105. grpHintActions = view.findViewById(R.id.grpHintActions);
  106. grpReady = view.findViewById(R.id.grpReady);
  107. fab = view.findViewById(R.id.fab);
  108. final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
  109. // Wire controls
  110. tvSupport.setOnClickListener(new View.OnClickListener() {
  111. @Override
  112. public void onClick(View v) {
  113. FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
  114. fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
  115. fragmentTransaction.commit();
  116. }
  117. });
  118. ibHintActions.setOnClickListener(new View.OnClickListener() {
  119. @Override
  120. public void onClick(View v) {
  121. prefs.edit().putBoolean("message_actions", true).apply();
  122. grpHintActions.setVisibility(View.GONE);
  123. }
  124. });
  125. ibHintSupport.setOnClickListener(new View.OnClickListener() {
  126. @Override
  127. public void onClick(View v) {
  128. prefs.edit().putBoolean("app_support", true).apply();
  129. grpHintSupport.setVisibility(View.GONE);
  130. }
  131. });
  132. rvMessage.setHasFixedSize(false);
  133. LinearLayoutManager llm = new LinearLayoutManager(getContext());
  134. rvMessage.setLayoutManager(llm);
  135. if (TextUtils.isEmpty(search))
  136. if (thread < 0)
  137. if (folder < 0)
  138. viewType = AdapterMessage.ViewType.UNIFIED;
  139. else
  140. viewType = AdapterMessage.ViewType.FOLDER;
  141. else
  142. viewType = AdapterMessage.ViewType.THREAD;
  143. else
  144. viewType = AdapterMessage.ViewType.SEARCH;
  145. adapter = new AdapterMessage(getContext(), getViewLifecycleOwner(), viewType);
  146. rvMessage.setAdapter(adapter);
  147. new ItemTouchHelper(new ItemTouchHelper.Callback() {
  148. @Override
  149. public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
  150. int pos = viewHolder.getAdapterPosition();
  151. if (pos == RecyclerView.NO_POSITION)
  152. return 0;
  153. TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
  154. if (EntityFolder.OUTBOX.equals(message.folderType))
  155. return 0;
  156. return makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
  157. }
  158. @Override
  159. public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
  160. return false;
  161. }
  162. @Override
  163. public void onChildDraw(Canvas canvas, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
  164. int pos = viewHolder.getAdapterPosition();
  165. if (pos == RecyclerView.NO_POSITION)
  166. return;
  167. TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
  168. boolean inbox = (EntityFolder.ARCHIVE.equals(message.folderType) || EntityFolder.TRASH.equals(message.folderType));
  169. View itemView = viewHolder.itemView;
  170. int margin = Math.round(12 * (getResources().getDisplayMetrics().density));
  171. if (dX > margin) {
  172. // Right swipe
  173. Drawable d = getResources().getDrawable(inbox ? R.drawable.baseline_inbox_24 : R.drawable.baseline_archive_24, getContext().getTheme());
  174. int padding = (itemView.getHeight() - d.getIntrinsicHeight());
  175. d.setBounds(
  176. itemView.getLeft() + margin,
  177. itemView.getTop() + padding / 2,
  178. itemView.getLeft() + margin + d.getIntrinsicWidth(),
  179. itemView.getTop() + padding / 2 + d.getIntrinsicHeight());
  180. d.draw(canvas);
  181. } else if (dX < -margin) {
  182. // Left swipe
  183. Drawable d = getResources().getDrawable(inbox ? R.drawable.baseline_inbox_24 : R.drawable.baseline_delete_24, getContext().getTheme());
  184. int padding = (itemView.getHeight() - d.getIntrinsicHeight());
  185. d.setBounds(
  186. itemView.getLeft() + itemView.getWidth() - d.getIntrinsicWidth() - margin,
  187. itemView.getTop() + padding / 2,
  188. itemView.getLeft() + itemView.getWidth() - margin,
  189. itemView.getTop() + padding / 2 + d.getIntrinsicHeight());
  190. d.draw(canvas);
  191. }
  192. super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
  193. }
  194. @Override
  195. public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
  196. int pos = viewHolder.getAdapterPosition();
  197. if (pos == RecyclerView.NO_POSITION)
  198. return;
  199. TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
  200. Log.i(Helper.TAG, "Swiped dir=" + direction + " message=" + message.id);
  201. Bundle args = new Bundle();
  202. args.putLong("id", message.id);
  203. args.putInt("direction", direction);
  204. new SimpleTask<String>() {
  205. @Override
  206. protected String onLoad(Context context, Bundle args) {
  207. long id = args.getLong("id");
  208. int direction = args.getInt("direction");
  209. EntityFolder target = null;
  210. // Get target folder and hide message
  211. DB db = DB.getInstance(context);
  212. try {
  213. db.beginTransaction();
  214. EntityMessage message = db.message().getMessage(id);
  215. EntityFolder folder = db.folder().getFolder(message.folder);
  216. if (EntityFolder.ARCHIVE.equals(folder.type) || EntityFolder.TRASH.equals(folder.type))
  217. target = db.folder().getFolderByType(message.account, EntityFolder.INBOX);
  218. else {
  219. if (direction == ItemTouchHelper.RIGHT)
  220. target = db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE);
  221. if (direction == ItemTouchHelper.LEFT || target == null)
  222. target = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
  223. }
  224. db.message().setMessageUiHide(message.id, true);
  225. db.setTransactionSuccessful();
  226. } finally {
  227. db.endTransaction();
  228. }
  229. Log.i(Helper.TAG, "Move id=" + id + " target=" + target);
  230. return target.name;
  231. }
  232. @Override
  233. protected void onLoaded(final Bundle args, final String target) {
  234. // Show undo snackbar
  235. final Snackbar snackbar = Snackbar.make(
  236. view,
  237. getString(R.string.title_moving, Helper.localizeFolderName(getContext(), target)),
  238. Snackbar.LENGTH_INDEFINITE);
  239. snackbar.setAction(R.string.title_undo, new View.OnClickListener() {
  240. @Override
  241. public void onClick(View v) {
  242. snackbar.dismiss();
  243. // Show message again
  244. new SimpleTask<Void>() {
  245. @Override
  246. protected Void onLoad(Context context, Bundle args) {
  247. long id = args.getLong("id");
  248. Log.i(Helper.TAG, "Undo move id=" + id);
  249. DB.getInstance(context).message().setMessageUiHide(id, false);
  250. return null;
  251. }
  252. @Override
  253. protected void onException(Bundle args, Throwable ex) {
  254. super.onException(args, ex);
  255. }
  256. }.load(FragmentMessages.this, args);
  257. }
  258. });
  259. snackbar.show();
  260. // Wait
  261. new Handler().postDelayed(new Runnable() {
  262. @Override
  263. public void run() {
  264. Log.i(Helper.TAG, "Move timeout shown=" + snackbar.isShown());
  265. // Remove snackbar
  266. if (snackbar.isShown())
  267. snackbar.dismiss();
  268. final Context context = getContext();
  269. args.putString("target", target);
  270. // Process move in a thread
  271. // - the fragment could be gone
  272. executor.submit(new Runnable() {
  273. @Override
  274. public void run() {
  275. try {
  276. long id = args.getLong("id");
  277. String target = args.getString("target");
  278. DB db = DB.getInstance(context);
  279. try {
  280. db.beginTransaction();
  281. EntityMessage message = db.message().getMessage(id);
  282. if (message != null && message.ui_hide) {
  283. Log.i(Helper.TAG, "Moving id=" + id + " target=" + target);
  284. EntityFolder folder = db.folder().getFolderByName(message.account, target);
  285. EntityOperation.queue(db, message, EntityOperation.MOVE, folder.id);
  286. }
  287. db.setTransactionSuccessful();
  288. } finally {
  289. db.endTransaction();
  290. }
  291. EntityOperation.process(context);
  292. } catch (Throwable ex) {
  293. Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  294. }
  295. }
  296. });
  297. }
  298. }, UNDO_TIMEOUT);
  299. }
  300. @Override
  301. protected void onException(Bundle args, Throwable ex) {
  302. Helper.unexpectedError(getContext(), ex);
  303. }
  304. }.load(FragmentMessages.this, args);
  305. }
  306. }).attachToRecyclerView(rvMessage);
  307. fab.setOnClickListener(new View.OnClickListener() {
  308. @Override
  309. public void onClick(View view) {
  310. startActivity(new Intent(getContext(), ActivityCompose.class)
  311. .putExtra("action", "new")
  312. .putExtra("account", (Long) fab.getTag())
  313. );
  314. }
  315. });
  316. // Initialize
  317. tvNoEmail.setVisibility(View.GONE);
  318. grpReady.setVisibility(View.GONE);
  319. pbWait.setVisibility(View.VISIBLE);
  320. fab.setVisibility(View.GONE);
  321. return view;
  322. }
  323. @Override
  324. public void onActivityCreated(@Nullable Bundle savedInstanceState) {
  325. super.onActivityCreated(savedInstanceState);
  326. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
  327. grpHintSupport.setVisibility(prefs.getBoolean("app_support", false) ? View.GONE : View.VISIBLE);
  328. grpHintActions.setVisibility(prefs.getBoolean("message_actions", false) ? View.GONE : View.VISIBLE);
  329. final DB db = DB.getInstance(getContext());
  330. // Primary account
  331. db.account().livePrimaryAccount().observe(getViewLifecycleOwner(), new Observer<EntityAccount>() {
  332. @Override
  333. public void onChanged(EntityAccount account) {
  334. primary = (account == null ? -1 : account.id);
  335. getActivity().invalidateOptionsMenu();
  336. }
  337. });
  338. // Folder
  339. switch (viewType) {
  340. case UNIFIED:
  341. db.folder().liveUnified().observe(getViewLifecycleOwner(), new Observer<List<TupleFolderEx>>() {
  342. @Override
  343. public void onChanged(List<TupleFolderEx> folders) {
  344. int unseen = 0;
  345. if (folders != null)
  346. for (TupleFolderEx folder : folders)
  347. unseen += folder.unseen;
  348. String name = getString(R.string.title_folder_unified);
  349. if (unseen > 0)
  350. setSubtitle(getString(R.string.title_folder_unseen, name, unseen));
  351. else
  352. setSubtitle(name);
  353. }
  354. });
  355. break;
  356. case FOLDER:
  357. db.folder().liveFolderEx(folder).observe(getViewLifecycleOwner(), new Observer<TupleFolderEx>() {
  358. @Override
  359. public void onChanged(@Nullable TupleFolderEx folder) {
  360. if (folder == null)
  361. setSubtitle(null);
  362. else {
  363. String name = Helper.localizeFolderName(getContext(), folder.name);
  364. if (folder.unseen > 0)
  365. setSubtitle(getString(R.string.title_folder_unseen, name, folder.unseen));
  366. else
  367. setSubtitle(name);
  368. }
  369. }
  370. });
  371. break;
  372. case THREAD:
  373. setSubtitle(R.string.title_folder_thread);
  374. break;
  375. case SEARCH:
  376. setSubtitle(getString(R.string.title_searching, search));
  377. break;
  378. }
  379. // Messages
  380. loadMessages();
  381. // Compose FAB
  382. Bundle args = new Bundle();
  383. args.putLong("folder", folder);
  384. args.putLong("thread", thread);
  385. new SimpleTask<Long>() {
  386. @Override
  387. protected Long onLoad(Context context, Bundle args) {
  388. long fid = args.getLong("folder", -1);
  389. long thread = args.getLong("thread", -1); // message ID
  390. DB db = DB.getInstance(context);
  391. Long account = null;
  392. if (thread < 0) {
  393. if (folder >= 0) {
  394. EntityFolder folder = db.folder().getFolder(fid);
  395. if (folder != null)
  396. account = folder.account;
  397. }
  398. } else {
  399. EntityMessage threaded = db.message().getMessage(thread);
  400. if (threaded != null)
  401. account = threaded.account;
  402. }
  403. if (account == null) {
  404. // outbox
  405. EntityFolder primary = db.folder().getPrimaryDrafts();
  406. if (primary != null)
  407. account = primary.account;
  408. }
  409. return account;
  410. }
  411. @Override
  412. protected void onLoaded(Bundle args, Long account) {
  413. if (account != null) {
  414. fab.setTag(account);
  415. fab.setVisibility(View.VISIBLE);
  416. }
  417. }
  418. @Override
  419. protected void onException(Bundle args, Throwable ex) {
  420. Helper.unexpectedError(getContext(), ex);
  421. }
  422. }.load(this, args);
  423. }
  424. @Override
  425. public void onResume() {
  426. super.onResume();
  427. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
  428. grpSupport.setVisibility(prefs.getBoolean("pro", false) ? View.GONE : View.VISIBLE);
  429. }
  430. @Override
  431. public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
  432. inflater.inflate(R.menu.menu_list, menu);
  433. final MenuItem menuSearch = menu.findItem(R.id.menu_search);
  434. final SearchView searchView = (SearchView) menuSearch.getActionView();
  435. searchView.setQueryHint(getString(R.string.title_search_hint));
  436. searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
  437. @Override
  438. public boolean onQueryTextSubmit(String query) {
  439. menuSearch.collapseActionView();
  440. if (PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("pro", false)) {
  441. Intent intent = new Intent();
  442. intent.putExtra("folder", folder);
  443. intent.putExtra("search", query);
  444. FragmentMessages fragment = new FragmentMessages();
  445. fragment.setArguments(intent.getExtras());
  446. FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
  447. fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("search");
  448. fragmentTransaction.commit();
  449. } else {
  450. FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
  451. fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
  452. fragmentTransaction.commit();
  453. }
  454. return true;
  455. }
  456. @Override
  457. public boolean onQueryTextChange(String newText) {
  458. return false;
  459. }
  460. });
  461. super.onCreateOptionsMenu(menu, inflater);
  462. }
  463. @Override
  464. public void onPrepareOptionsMenu(Menu menu) {
  465. menu.findItem(R.id.menu_search).setVisible(folder >= 0 && search == null);
  466. menu.findItem(R.id.menu_sort_on).setVisible(TextUtils.isEmpty(search));
  467. menu.findItem(R.id.menu_folders).setVisible(primary >= 0);
  468. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
  469. String sort = prefs.getString("sort", "time");
  470. if ("time".equals(sort))
  471. menu.findItem(R.id.menu_sort_on_time).setChecked(true);
  472. else if ("unread".equals(sort))
  473. menu.findItem(R.id.menu_sort_on_unread).setChecked(true);
  474. else if ("starred".equals(sort))
  475. menu.findItem(R.id.menu_sort_on_starred).setChecked(true);
  476. super.onPrepareOptionsMenu(menu);
  477. }
  478. @Override
  479. public boolean onOptionsItemSelected(MenuItem item) {
  480. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
  481. switch (item.getItemId()) {
  482. case R.id.menu_sort_on_time:
  483. prefs.edit().putString("sort", "time").apply();
  484. item.setChecked(true);
  485. loadMessages();
  486. return true;
  487. case R.id.menu_sort_on_unread:
  488. case R.id.menu_sort_on_starred:
  489. if (prefs.getBoolean("pro", false)) {
  490. prefs.edit().putString("sort", item.getItemId() == R.id.menu_sort_on_unread ? "unread" : "starred").apply();
  491. item.setChecked(true);
  492. loadMessages();
  493. } else {
  494. FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
  495. fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
  496. fragmentTransaction.commit();
  497. }
  498. return true;
  499. case R.id.menu_folders:
  500. onMenuFolders();
  501. loadMessages();
  502. return true;
  503. default:
  504. return super.onOptionsItemSelected(item);
  505. }
  506. }
  507. private void onMenuFolders() {
  508. getFragmentManager().popBackStack("unified", 0);
  509. Bundle args = new Bundle();
  510. args.putLong("account", primary);
  511. FragmentFolders fragment = new FragmentFolders();
  512. fragment.setArguments(args);
  513. FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
  514. fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("folders");
  515. fragmentTransaction.commit();
  516. }
  517. private void loadMessages() {
  518. final DB db = DB.getInstance(getContext());
  519. // Observe folder/messages/search
  520. if (TextUtils.isEmpty(search)) {
  521. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
  522. String sort = prefs.getString("sort", "time");
  523. boolean debug = prefs.getBoolean("debug", false);
  524. if (messages != null)
  525. messages.removeObservers(getViewLifecycleOwner());
  526. switch (viewType) {
  527. case UNIFIED:
  528. messages = new LivePagedListBuilder<>(db.message().pagedUnifiedInbox(sort, debug), MESSAGES_PAGE_SIZE).build();
  529. break;
  530. case FOLDER:
  531. messages = new LivePagedListBuilder<>(db.message().pagedFolder(folder, sort, false, debug), MESSAGES_PAGE_SIZE).build();
  532. break;
  533. case THREAD:
  534. messages = new LivePagedListBuilder<>(db.message().pagedThread(thread, sort, debug), MESSAGES_PAGE_SIZE).build();
  535. break;
  536. }
  537. messages.observe(getViewLifecycleOwner(), new Observer<PagedList<TupleMessageEx>>() {
  538. @Override
  539. public void onChanged(@Nullable PagedList<TupleMessageEx> messages) {
  540. if (messages == null) {
  541. finish();
  542. return;
  543. }
  544. Log.i(Helper.TAG, "Submit messages=" + messages.size());
  545. adapter.submitList(messages);
  546. pbWait.setVisibility(View.GONE);
  547. grpReady.setVisibility(View.VISIBLE);
  548. if (messages.size() == 0) {
  549. tvNoEmail.setVisibility(View.VISIBLE);
  550. rvMessage.setVisibility(View.GONE);
  551. } else {
  552. tvNoEmail.setVisibility(View.GONE);
  553. rvMessage.setVisibility(View.VISIBLE);
  554. }
  555. }
  556. });
  557. } else {
  558. Log.i(Helper.TAG, "Search state=" + searchState);
  559. if (searchCallback == null)
  560. searchCallback = new BoundaryCallbackMessages(
  561. getContext(), FragmentMessages.this,
  562. folder, search,
  563. new BoundaryCallbackMessages.IBoundaryCallbackMessages() {
  564. @Override
  565. public void onLoading() {
  566. pbWait.setVisibility(View.VISIBLE);
  567. }
  568. @Override
  569. public void onLoaded() {
  570. pbWait.setVisibility(View.GONE);
  571. }
  572. @Override
  573. public void onError(Context context, Throwable ex) {
  574. Helper.unexpectedError(context, ex);
  575. }
  576. });
  577. Bundle args = new Bundle();
  578. args.putLong("folder", folder);
  579. args.putString("search", search);
  580. new SimpleTask<Void>() {
  581. @Override
  582. protected Void onLoad(Context context, Bundle args) {
  583. if (searchState == SearchState.Reset) {
  584. long folder = args.getLong("folder");
  585. DB.getInstance(context).message().resetFound(folder);
  586. searchState = SearchState.Database;
  587. Log.i(Helper.TAG, "Search reset done");
  588. }
  589. return null;
  590. }
  591. @Override
  592. protected void onLoaded(final Bundle args, Void data) {
  593. LivePagedListBuilder<Integer, TupleMessageEx> builder = new LivePagedListBuilder<>(db.message().pagedFolder(folder, "time", true, false), SEARCH_PAGE_SIZE);
  594. builder.setBoundaryCallback(searchCallback);
  595. LiveData<PagedList<TupleMessageEx>> messages = builder.build();
  596. messages.observe(getViewLifecycleOwner(), new Observer<PagedList<TupleMessageEx>>() {
  597. @Override
  598. public void onChanged(PagedList<TupleMessageEx> messages) {
  599. Log.i(Helper.TAG, "Submit found messages=" + messages.size());
  600. adapter.submitList(messages);
  601. grpReady.setVisibility(View.VISIBLE);
  602. }
  603. });
  604. new SimpleTask<Long>() {
  605. @Override
  606. protected Long onLoad(Context context, Bundle args) throws Throwable {
  607. long last = 0;
  608. if (searchState == SearchState.Database) {
  609. last = new Date().getTime();
  610. long folder = args.getLong("folder");
  611. String search = args.getString("search").toLowerCase();
  612. DB db = DB.getInstance(context);
  613. for (long id : db.message().getMessageIDs(folder)) {
  614. EntityMessage message = db.message().getMessage(id);
  615. if (message != null) { // Message could be removed in the meantime
  616. String from = MessageHelper.getFormattedAddresses(message.from, true);
  617. if (from.toLowerCase().contains(search) ||
  618. message.subject.toLowerCase().contains(search) ||
  619. message.read(context).toLowerCase().contains(search)) {
  620. Log.i(Helper.TAG, "Search found id=" + id);
  621. db.message().setMessageFound(message.id, true);
  622. last = message.received;
  623. }
  624. }
  625. }
  626. searchState = SearchState.Boundary;
  627. Log.i(Helper.TAG, "Search database done");
  628. }
  629. return last;
  630. }
  631. @Override
  632. protected void onLoaded(Bundle args, Long last) {
  633. pbWait.setVisibility(View.GONE);
  634. searchCallback.setEnabled(true);
  635. if (last > 0)
  636. searchCallback.load(last);
  637. }
  638. }.load(FragmentMessages.this, args);
  639. }
  640. }.load(this, args);
  641. }
  642. }
  643. void onNewMessages() {
  644. rvMessage.scrollToPosition(0);
  645. }
  646. }