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

558 lines
21 KiB

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.BroadcastReceiver;
  17. import android.content.Context;
  18. import android.content.Intent;
  19. import android.content.IntentFilter;
  20. import android.content.res.Configuration;
  21. import android.net.Uri;
  22. import android.os.Build;
  23. import android.os.Bundle;
  24. import android.preference.PreferenceManager;
  25. import android.util.Log;
  26. import android.view.LayoutInflater;
  27. import android.view.MenuItem;
  28. import android.view.View;
  29. import android.view.ViewGroup;
  30. import android.widget.AdapterView;
  31. import android.widget.ArrayAdapter;
  32. import android.widget.ImageView;
  33. import android.widget.ListView;
  34. import android.widget.TextView;
  35. import android.widget.Toast;
  36. import java.io.BufferedReader;
  37. import java.io.File;
  38. import java.io.FileReader;
  39. import java.text.Collator;
  40. import java.util.ArrayList;
  41. import java.util.Collections;
  42. import java.util.Comparator;
  43. import java.util.Date;
  44. import java.util.List;
  45. import java.util.Locale;
  46. import javax.mail.Address;
  47. import javax.mail.internet.InternetAddress;
  48. import androidx.annotation.NonNull;
  49. import androidx.annotation.Nullable;
  50. import androidx.appcompat.app.ActionBarDrawerToggle;
  51. import androidx.drawerlayout.widget.DrawerLayout;
  52. import androidx.fragment.app.FragmentManager;
  53. import androidx.fragment.app.FragmentTransaction;
  54. import androidx.lifecycle.Observer;
  55. import androidx.localbroadcastmanager.content.LocalBroadcastManager;
  56. public class ActivityView extends ActivityBase implements FragmentManager.OnBackStackChangedListener {
  57. private DrawerLayout drawerLayout;
  58. private ListView drawerList;
  59. private ActionBarDrawerToggle drawerToggle;
  60. static final int REQUEST_VIEW = 1;
  61. static final int REQUEST_UNSEEN = 2;
  62. static final String ACTION_VIEW_MESSAGES = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGES";
  63. static final String ACTION_VIEW_MESSAGE = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGE";
  64. static final String ACTION_EDIT_FOLDER = BuildConfig.APPLICATION_ID + ".EDIT_FOLDER";
  65. @Override
  66. protected void onCreate(Bundle savedInstanceState) {
  67. super.onCreate(savedInstanceState);
  68. setContentView(R.layout.activity_view);
  69. getSupportActionBar().setDisplayHomeAsUpEnabled(true);
  70. getSupportActionBar().setHomeButtonEnabled(true);
  71. drawerLayout = findViewById(R.id.drawer_layout);
  72. drawerLayout.setScrimColor(Helper.resolveColor(this, R.attr.colorDrawerScrim));
  73. drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.app_name, R.string.app_name) {
  74. public void onDrawerClosed(View view) {
  75. super.onDrawerClosed(view);
  76. getSupportActionBar().setTitle(getString(R.string.app_name));
  77. }
  78. public void onDrawerOpened(View drawerView) {
  79. super.onDrawerOpened(drawerView);
  80. getSupportActionBar().setTitle(getString(R.string.app_name));
  81. }
  82. };
  83. drawerLayout.addDrawerListener(drawerToggle);
  84. drawerList = findViewById(R.id.drawer_list);
  85. drawerList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
  86. @Override
  87. public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
  88. DrawerItem item = (DrawerItem) parent.getAdapter().getItem(position);
  89. switch (item.getId()) {
  90. case -1:
  91. onMenuFolders((long) item.getData());
  92. break;
  93. case R.string.menu_setup:
  94. onMenuSetup();
  95. break;
  96. case R.string.menu_operations:
  97. onMenuOperations();
  98. break;
  99. case R.string.menu_privacy:
  100. onMenuPrivacy();
  101. break;
  102. case R.string.menu_faq:
  103. onMenuFAQ();
  104. break;
  105. case R.string.menu_about:
  106. onMenuAbout();
  107. break;
  108. }
  109. drawerLayout.closeDrawer(drawerList);
  110. }
  111. });
  112. getSupportFragmentManager().addOnBackStackChangedListener(this);
  113. DB.getInstance(this).account().liveAccounts().observe(this, new Observer<List<EntityAccount>>() {
  114. @Override
  115. public void onChanged(@Nullable List<EntityAccount> accounts) {
  116. if (accounts == null)
  117. accounts = new ArrayList<>();
  118. ArrayAdapterDrawer drawerArray = new ArrayAdapterDrawer(ActivityView.this, R.layout.item_drawer);
  119. final Collator collator = Collator.getInstance(Locale.getDefault());
  120. collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc
  121. Collections.sort(accounts, new Comparator<EntityAccount>() {
  122. @Override
  123. public int compare(EntityAccount a1, EntityAccount a2) {
  124. return collator.compare(a1.name, a2.name);
  125. }
  126. });
  127. for (EntityAccount account : accounts)
  128. drawerArray.add(new DrawerItem(-1, R.drawable.baseline_folder_24, account.name, account.id));
  129. drawerArray.add(new DrawerItem(ActivityView.this, R.drawable.baseline_settings_applications_24, R.string.menu_setup));
  130. if (PreferenceManager.getDefaultSharedPreferences(ActivityView.this).getBoolean("debug", false))
  131. drawerArray.add(new DrawerItem(ActivityView.this, R.drawable.baseline_list_24, R.string.menu_operations));
  132. if (getIntentPrivacy().resolveActivity(getPackageManager()) != null)
  133. drawerArray.add(new DrawerItem(ActivityView.this, R.drawable.baseline_account_box_24, R.string.menu_privacy));
  134. if (getIntentFAQ().resolveActivity(getPackageManager()) != null)
  135. drawerArray.add(new DrawerItem(ActivityView.this, R.drawable.baseline_question_answer_24, R.string.menu_faq));
  136. drawerArray.add(new DrawerItem(ActivityView.this, R.drawable.baseline_help_24, R.string.menu_about));
  137. drawerList.setAdapter(drawerArray);
  138. }
  139. });
  140. if (getSupportFragmentManager().getFragments().size() == 0) {
  141. Bundle args = new Bundle();
  142. args.putLong("folder", -1);
  143. FragmentMessages fragment = new FragmentMessages();
  144. fragment.setArguments(args);
  145. FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
  146. fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("unified");
  147. fragmentTransaction.commit();
  148. }
  149. new SimpleTask<Long>() {
  150. @Override
  151. protected Long onLoad(Context context, Bundle args) throws Throwable {
  152. File file = new File(context.getCacheDir(), "crash.log");
  153. if (file.exists()) {
  154. Address to = new InternetAddress("marcel+email@faircode.eu", "FairCode");
  155. // Get version info
  156. StringBuilder sb = new StringBuilder();
  157. sb.append(String.format("%s: %s/%d\r\n", BuildConfig.APPLICATION_ID, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
  158. sb.append(String.format("Android: %s (SDK %d)\r\n", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
  159. sb.append("\r\n");
  160. // Get device info
  161. sb.append(String.format("Brand: %s\r\n", Build.BRAND));
  162. sb.append(String.format("Manufacturer: %s\r\n", Build.MANUFACTURER));
  163. sb.append(String.format("Model: %s\r\n", Build.MODEL));
  164. sb.append(String.format("Product: %s\r\n", Build.PRODUCT));
  165. sb.append(String.format("Device: %s\r\n", Build.DEVICE));
  166. sb.append(String.format("Host: %s\r\n", Build.HOST));
  167. sb.append(String.format("Display: %s\r\n", Build.DISPLAY));
  168. sb.append(String.format("Id: %s\r\n", Build.ID));
  169. sb.append("\r\n");
  170. BufferedReader in = null;
  171. try {
  172. String line;
  173. in = new BufferedReader(new FileReader(file));
  174. while ((line = in.readLine()) != null)
  175. sb.append(line);
  176. } finally {
  177. if (in != null)
  178. in.close();
  179. }
  180. file.delete();
  181. EntityMessage draft = null;
  182. DB db = DB.getInstance(context);
  183. try {
  184. db.beginTransaction();
  185. EntityFolder drafts = db.folder().getPrimaryDrafts();
  186. if (drafts != null) {
  187. draft = new EntityMessage();
  188. draft.account = drafts.account;
  189. draft.folder = drafts.id;
  190. draft.msgid = draft.generateMessageId();
  191. draft.to = new Address[]{to};
  192. draft.subject = context.getString(R.string.app_name) + " crash log";
  193. draft.body = "<pre>" + sb.toString().replaceAll("\\r?\\n", "<br />") + "</pre>";
  194. draft.received = new Date().getTime();
  195. draft.seen = false;
  196. draft.ui_seen = false;
  197. draft.ui_hide = false;
  198. draft.id = db.message().insertMessage(draft);
  199. }
  200. EntityOperation.queue(db, draft, EntityOperation.ADD);
  201. db.setTransactionSuccessful();
  202. } finally {
  203. db.endTransaction();
  204. }
  205. EntityOperation.process(context);
  206. return (draft == null ? null : draft.id);
  207. }
  208. return null;
  209. }
  210. @Override
  211. protected void onLoaded(Bundle args, Long id) {
  212. if (id != null)
  213. startActivity(
  214. new Intent(ActivityView.this, ActivityCompose.class)
  215. .putExtra("action", "edit")
  216. .putExtra("id", id));
  217. }
  218. }.load(this, new Bundle());
  219. checkIntent(getIntent());
  220. }
  221. @Override
  222. protected void onPostCreate(Bundle savedInstanceState) {
  223. super.onPostCreate(savedInstanceState);
  224. drawerToggle.syncState();
  225. }
  226. @Override
  227. protected void onNewIntent(Intent intent) {
  228. if (intent.getBooleanExtra("setup", false))
  229. intent.getExtras().remove("setup");
  230. else
  231. getSupportFragmentManager().popBackStack("unified", 0);
  232. checkIntent(intent);
  233. super.onNewIntent(intent);
  234. }
  235. @Override
  236. protected void onResume() {
  237. super.onResume();
  238. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
  239. IntentFilter iff = new IntentFilter();
  240. iff.addAction(ACTION_VIEW_MESSAGES);
  241. iff.addAction(ACTION_VIEW_MESSAGE);
  242. iff.addAction(ACTION_EDIT_FOLDER);
  243. lbm.registerReceiver(receiver, iff);
  244. }
  245. @Override
  246. protected void onPause() {
  247. super.onPause();
  248. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
  249. lbm.unregisterReceiver(receiver);
  250. }
  251. @Override
  252. public void onConfigurationChanged(Configuration newConfig) {
  253. super.onConfigurationChanged(newConfig);
  254. drawerToggle.onConfigurationChanged(newConfig);
  255. }
  256. @Override
  257. protected void onDestroy() {
  258. super.onDestroy();
  259. }
  260. @Override
  261. public void onBackPressed() {
  262. if (drawerLayout.isDrawerOpen(drawerList))
  263. drawerLayout.closeDrawer(drawerList);
  264. else
  265. super.onBackPressed();
  266. }
  267. @Override
  268. public void onBackStackChanged() {
  269. int count = getSupportFragmentManager().getBackStackEntryCount();
  270. if (count == 0)
  271. finish();
  272. else
  273. drawerToggle.setDrawerIndicatorEnabled(count == 1);
  274. }
  275. @Override
  276. public boolean onOptionsItemSelected(MenuItem item) {
  277. if (drawerToggle.onOptionsItemSelected(item))
  278. return true;
  279. switch (item.getItemId()) {
  280. case android.R.id.home:
  281. getSupportFragmentManager().popBackStack();
  282. return true;
  283. default:
  284. return false;
  285. }
  286. }
  287. private void checkIntent(Intent intent) {
  288. Log.i(Helper.TAG, "View intent=" + intent + " action=" + intent.getAction());
  289. String action = intent.getAction();
  290. intent.setAction(null);
  291. setIntent(intent);
  292. if ("unseen".equals(action)) {
  293. Bundle args = new Bundle();
  294. args.putLong("time", new Date().getTime());
  295. new SimpleTask<Void>() {
  296. @Override
  297. protected Void onLoad(Context context, Bundle args) {
  298. long time = args.getLong("time");
  299. DB db = DB.getInstance(context);
  300. try {
  301. db.beginTransaction();
  302. for (EntityAccount account : db.account().getAccounts(true))
  303. db.account().setAccountSeenUntil(account.id, time);
  304. db.setTransactionSuccessful();
  305. } finally {
  306. db.endTransaction();
  307. }
  308. return null;
  309. }
  310. @Override
  311. protected void onException(Bundle args, Throwable ex) {
  312. Toast.makeText(ActivityView.this, ex.toString(), Toast.LENGTH_LONG).show();
  313. }
  314. }.load(this, args);
  315. }
  316. }
  317. private Intent getIntentPrivacy() {
  318. Intent intent = new Intent(Intent.ACTION_VIEW);
  319. intent.setData(Uri.parse("https://email.faircode.eu/privacy/"));
  320. return intent;
  321. }
  322. private Intent getIntentFAQ() {
  323. Intent intent = new Intent(Intent.ACTION_VIEW);
  324. intent.setData(Uri.parse("https://github.com/M66B/open-source-email/blob/master/FAQ.md"));
  325. return intent;
  326. }
  327. private void onMenuFolders(long account) {
  328. getSupportFragmentManager().popBackStack("unified", 0);
  329. Bundle args = new Bundle();
  330. args.putLong("account", account);
  331. FragmentFolders fragment = new FragmentFolders();
  332. fragment.setArguments(args);
  333. FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
  334. fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("folders");
  335. fragmentTransaction.commit();
  336. }
  337. private void onMenuSetup() {
  338. startActivity(new Intent(ActivityView.this, ActivitySetup.class));
  339. }
  340. private void onMenuOperations() {
  341. FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
  342. fragmentTransaction.replace(R.id.content_frame, new FragmentOperations()).addToBackStack("operations");
  343. fragmentTransaction.commit();
  344. }
  345. private void onMenuPrivacy() {
  346. startActivity(getIntentPrivacy());
  347. }
  348. private void onMenuFAQ() {
  349. startActivity(getIntentFAQ());
  350. }
  351. private void onMenuAbout() {
  352. FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
  353. fragmentTransaction.replace(R.id.content_frame, new FragmentAbout()).addToBackStack("about");
  354. fragmentTransaction.commit();
  355. }
  356. private class DrawerItem {
  357. private int id;
  358. private int icon;
  359. private String title;
  360. private Object data;
  361. DrawerItem(Context context, int icon, int title) {
  362. this.id = title;
  363. this.icon = icon;
  364. this.title = context.getString(title);
  365. }
  366. DrawerItem(int id, int icon, String title, Object data) {
  367. this.id = id;
  368. this.icon = icon;
  369. this.title = title;
  370. this.data = data;
  371. }
  372. public int getId() {
  373. return this.id;
  374. }
  375. public Object getData() {
  376. return this.data;
  377. }
  378. }
  379. private static class ArrayAdapterDrawer extends ArrayAdapter<DrawerItem> {
  380. private int resource;
  381. ArrayAdapterDrawer(@NonNull Context context, int resource) {
  382. super(context, resource);
  383. this.resource = resource;
  384. }
  385. @NonNull
  386. public View getView(int position, View convertView, @NonNull ViewGroup parent) {
  387. View row;
  388. if (null == convertView)
  389. row = LayoutInflater.from(getContext()).inflate(this.resource, null);
  390. else
  391. row = convertView;
  392. DrawerItem item = getItem(position);
  393. ImageView iv = row.findViewById(R.id.ivItem);
  394. TextView tv = row.findViewById(R.id.tvItem);
  395. iv.setImageResource(item.icon);
  396. tv.setText(item.title);
  397. return row;
  398. }
  399. }
  400. BroadcastReceiver receiver = new BroadcastReceiver() {
  401. @Override
  402. public void onReceive(Context context, Intent intent) {
  403. if (ACTION_VIEW_MESSAGES.equals(intent.getAction())) {
  404. FragmentMessages fragment = new FragmentMessages();
  405. fragment.setArguments(intent.getExtras());
  406. FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
  407. fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("messages");
  408. fragmentTransaction.commit();
  409. } else if (ACTION_VIEW_MESSAGE.equals(intent.getAction())) {
  410. new SimpleTask<Void>() {
  411. @Override
  412. protected Void onLoad(Context context, Bundle args) {
  413. long id = args.getLong("id");
  414. DB db = DB.getInstance(context);
  415. try {
  416. db.beginTransaction();
  417. EntityMessage message = db.message().getMessage(id);
  418. EntityFolder folder = db.folder().getFolder(message.folder);
  419. if (!EntityFolder.OUTBOX.equals(folder.type))
  420. for (EntityMessage tmessage : db.message().getMessageByThread(message.account, message.thread)) {
  421. db.message().setMessageUiSeen(tmessage.id, true);
  422. EntityOperation.queue(db, tmessage, EntityOperation.SEEN, true);
  423. }
  424. db.setTransactionSuccessful();
  425. } finally {
  426. db.endTransaction();
  427. }
  428. EntityOperation.process(context);
  429. return null;
  430. }
  431. @Override
  432. protected void onLoaded(Bundle args, Void result) {
  433. FragmentMessage fragment = new FragmentMessage();
  434. fragment.setArguments(args);
  435. FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
  436. fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("message");
  437. fragmentTransaction.commit();
  438. }
  439. @Override
  440. protected void onException(Bundle args, Throwable ex) {
  441. Toast.makeText(ActivityView.this, ex.toString(), Toast.LENGTH_LONG).show();
  442. }
  443. }.load(ActivityView.this, intent.getExtras());
  444. } else if (ACTION_EDIT_FOLDER.equals(intent.getAction())) {
  445. FragmentFolder fragment = new FragmentFolder();
  446. fragment.setArguments(intent.getExtras());
  447. FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
  448. fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("folder");
  449. fragmentTransaction.commit();
  450. }
  451. }
  452. };
  453. }