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.

1286 lines
58 KiB

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.app.Notification;
  17. import android.app.NotificationChannel;
  18. import android.app.NotificationManager;
  19. import android.app.PendingIntent;
  20. import android.arch.lifecycle.LifecycleService;
  21. import android.arch.lifecycle.Observer;
  22. import android.content.BroadcastReceiver;
  23. import android.content.Context;
  24. import android.content.Intent;
  25. import android.content.IntentFilter;
  26. import android.media.RingtoneManager;
  27. import android.net.ConnectivityManager;
  28. import android.net.Network;
  29. import android.net.NetworkCapabilities;
  30. import android.net.NetworkRequest;
  31. import android.net.Uri;
  32. import android.os.Build;
  33. import android.os.SystemClock;
  34. import android.support.annotation.Nullable;
  35. import android.support.v4.content.ContextCompat;
  36. import android.support.v4.content.LocalBroadcastManager;
  37. import android.text.TextUtils;
  38. import android.util.Log;
  39. import com.sun.mail.iap.ProtocolException;
  40. import com.sun.mail.imap.IMAPFolder;
  41. import com.sun.mail.imap.IMAPMessage;
  42. import com.sun.mail.imap.IMAPStore;
  43. import com.sun.mail.imap.protocol.IMAPProtocol;
  44. import com.sun.mail.smtp.SMTPSendFailedException;
  45. import org.json.JSONArray;
  46. import org.json.JSONException;
  47. import java.io.ByteArrayOutputStream;
  48. import java.io.IOException;
  49. import java.io.InputStream;
  50. import java.util.ArrayList;
  51. import java.util.Calendar;
  52. import java.util.Date;
  53. import java.util.HashMap;
  54. import java.util.List;
  55. import java.util.Map;
  56. import java.util.Properties;
  57. import java.util.concurrent.ExecutorService;
  58. import java.util.concurrent.Executors;
  59. import javax.mail.Address;
  60. import javax.mail.FetchProfile;
  61. import javax.mail.Flags;
  62. import javax.mail.Folder;
  63. import javax.mail.FolderClosedException;
  64. import javax.mail.FolderNotFoundException;
  65. import javax.mail.Message;
  66. import javax.mail.MessageRemovedException;
  67. import javax.mail.MessagingException;
  68. import javax.mail.Session;
  69. import javax.mail.Transport;
  70. import javax.mail.UIDFolder;
  71. import javax.mail.event.ConnectionAdapter;
  72. import javax.mail.event.ConnectionEvent;
  73. import javax.mail.event.FolderAdapter;
  74. import javax.mail.event.FolderEvent;
  75. import javax.mail.event.MessageChangedEvent;
  76. import javax.mail.event.MessageChangedListener;
  77. import javax.mail.event.MessageCountAdapter;
  78. import javax.mail.event.MessageCountEvent;
  79. import javax.mail.event.StoreEvent;
  80. import javax.mail.event.StoreListener;
  81. import javax.mail.internet.InternetAddress;
  82. import javax.mail.internet.MimeMessage;
  83. import javax.mail.search.ComparisonTerm;
  84. import javax.mail.search.ReceivedDateTerm;
  85. public class ServiceSynchronize extends LifecycleService {
  86. private ServiceState state = new ServiceState();
  87. private ExecutorService executor = Executors.newSingleThreadExecutor();
  88. private static final int NOTIFICATION_SYNCHRONIZE = 1;
  89. private static final int NOTIFICATION_UNSEEN = 2;
  90. private static final long NOOP_INTERVAL = 9 * 60 * 1000L; // ms
  91. private static final int FETCH_BATCH_SIZE = 10;
  92. private static final int DOWNLOAD_BUFFER_SIZE = 8192; // bytes
  93. static final String ACTION_PROCESS_FOLDER = BuildConfig.APPLICATION_ID + ".PROCESS_FOLDER";
  94. static final String ACTION_PROCESS_OUTBOX = BuildConfig.APPLICATION_ID + ".PROCESS_OUTBOX";
  95. private class ServiceState {
  96. boolean running = false;
  97. List<Thread> threads = new ArrayList<>(); // accounts
  98. }
  99. public ServiceSynchronize() {
  100. // https://docs.oracle.com/javaee/6/api/javax/mail/internet/package-summary.html
  101. System.setProperty("mail.mime.ignoreunknownencoding", "true");
  102. System.setProperty("mail.mime.decodefilename", "true");
  103. System.setProperty("mail.mime.encodefilename", "true");
  104. }
  105. @Override
  106. public void onCreate() {
  107. Log.i(Helper.TAG, "Service create");
  108. super.onCreate();
  109. startForeground(NOTIFICATION_SYNCHRONIZE, getNotificationService(0, 0).build());
  110. // Listen for network changes
  111. ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
  112. NetworkRequest.Builder builder = new NetworkRequest.Builder();
  113. builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
  114. builder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
  115. cm.registerNetworkCallback(builder.build(), networkCallback);
  116. DB.getInstance(this).account().liveStats().observe(this, new Observer<TupleAccountStats>() {
  117. private int prev_unseen = -1;
  118. @Override
  119. public void onChanged(@Nullable TupleAccountStats stats) {
  120. if (stats != null) {
  121. NotificationManager nm = getSystemService(NotificationManager.class);
  122. nm.notify(NOTIFICATION_SYNCHRONIZE,
  123. getNotificationService(stats.accounts, stats.operations).build());
  124. if (stats.unseen > 0) {
  125. if (stats.unseen > prev_unseen) {
  126. nm.cancel(NOTIFICATION_UNSEEN);
  127. nm.notify(NOTIFICATION_UNSEEN, getNotificationUnseen(stats.unseen).build());
  128. }
  129. } else
  130. nm.cancel(NOTIFICATION_UNSEEN);
  131. prev_unseen = stats.unseen;
  132. }
  133. }
  134. });
  135. }
  136. @Override
  137. public void onDestroy() {
  138. Log.i(Helper.TAG, "Service destroy");
  139. ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
  140. cm.unregisterNetworkCallback(networkCallback);
  141. networkCallback.onLost(cm.getActiveNetwork());
  142. stopForeground(true);
  143. NotificationManager nm = getSystemService(NotificationManager.class);
  144. nm.cancel(NOTIFICATION_SYNCHRONIZE);
  145. super.onDestroy();
  146. }
  147. @Override
  148. public int onStartCommand(Intent intent, int flags, int startId) {
  149. Log.i(Helper.TAG, "Service start");
  150. super.onStartCommand(intent, flags, startId);
  151. if ("unseen".equals(intent.getAction())) {
  152. final long now = new Date().getTime();
  153. executor.submit(new Runnable() {
  154. @Override
  155. public void run() {
  156. DaoAccount dao = DB.getInstance(ServiceSynchronize.this).account();
  157. for (EntityAccount account : dao.getAccounts(true)) {
  158. account.seen_until = now;
  159. dao.updateAccount(account);
  160. }
  161. Log.i(Helper.TAG, "Updated seen until");
  162. }
  163. });
  164. }
  165. return START_STICKY;
  166. }
  167. private Notification.Builder getNotificationService(int accounts, int operations) {
  168. // Build pending intent
  169. Intent intent = new Intent(this, ActivityView.class);
  170. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  171. PendingIntent pi = PendingIntent.getActivity(
  172. this, ActivityView.REQUEST_VIEW, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  173. // Build notification
  174. Notification.Builder builder;
  175. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
  176. builder = new Notification.Builder(this, "service");
  177. else
  178. builder = new Notification.Builder(this);
  179. builder
  180. .setSmallIcon(R.drawable.baseline_mail_outline_24)
  181. .setContentTitle(getString(R.string.title_notification_synchronizing, accounts))
  182. .setContentText(getString(R.string.title_notification_operations, operations))
  183. .setContentIntent(pi)
  184. .setAutoCancel(false)
  185. .setShowWhen(false)
  186. .setPriority(Notification.PRIORITY_MIN)
  187. .setCategory(Notification.CATEGORY_STATUS)
  188. .setVisibility(Notification.VISIBILITY_SECRET);
  189. return builder;
  190. }
  191. private Notification.Builder getNotificationUnseen(int unseen) {
  192. // Build pending intent
  193. Intent intent = new Intent(this, ActivityView.class);
  194. intent.setAction("unseen");
  195. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  196. PendingIntent pi = PendingIntent.getActivity(
  197. this, ActivityView.REQUEST_UNSEEN, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  198. Intent delete = new Intent(this, ServiceSynchronize.class);
  199. delete.setAction("unseen");
  200. PendingIntent pid = PendingIntent.getService(this, 1, delete, PendingIntent.FLAG_UPDATE_CURRENT);
  201. Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
  202. // Build notification
  203. Notification.Builder builder;
  204. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
  205. builder = new Notification.Builder(this, "notification");
  206. else
  207. builder = new Notification.Builder(this);
  208. builder
  209. .setSmallIcon(R.drawable.baseline_mail_24)
  210. .setContentTitle(getString(R.string.title_notification_unseen, unseen))
  211. .setContentIntent(pi)
  212. .setSound(uri)
  213. .setShowWhen(false)
  214. .setPriority(Notification.PRIORITY_DEFAULT)
  215. .setCategory(Notification.CATEGORY_STATUS)
  216. .setVisibility(Notification.VISIBILITY_PUBLIC)
  217. .setDeleteIntent(pid);
  218. return builder;
  219. }
  220. private Notification.Builder getNotificationError(String action, Throwable ex) {
  221. // Build pending intent
  222. Intent intent = new Intent(this, ActivityView.class);
  223. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  224. PendingIntent pi = PendingIntent.getActivity(
  225. this, ActivityView.REQUEST_VIEW, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  226. // Build notification
  227. Notification.Builder builder;
  228. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
  229. builder = new Notification.Builder(this, "error");
  230. else
  231. builder = new Notification.Builder(this);
  232. builder
  233. .setSmallIcon(android.R.drawable.stat_notify_error)
  234. .setContentTitle(getString(R.string.title_notification_failed, action))
  235. .setContentText(Helper.formatThrowable(ex))
  236. .setContentIntent(pi)
  237. .setAutoCancel(false)
  238. .setShowWhen(true)
  239. .setPriority(Notification.PRIORITY_MAX)
  240. .setCategory(Notification.CATEGORY_ERROR)
  241. .setVisibility(Notification.VISIBILITY_SECRET);
  242. return builder;
  243. }
  244. private void reportError(String account, String folder, Throwable ex) {
  245. String action = account + "/" + folder;
  246. if (!(ex instanceof IllegalStateException) && // This operation is not allowed on a closed folder
  247. !(ex instanceof FolderClosedException)) {
  248. NotificationManager nm = getSystemService(NotificationManager.class);
  249. nm.notify(action, 1, getNotificationError(action, ex).build());
  250. }
  251. }
  252. private void monitorAccount(final EntityAccount account) {
  253. Log.i(Helper.TAG, account.name + " start ");
  254. while (state.running) {
  255. IMAPStore istore = null;
  256. try {
  257. Properties props = MessageHelper.getSessionProperties();
  258. props.put("mail.imaps.peek", "true");
  259. props.setProperty("mail.mime.address.strict", "false");
  260. //props.put("mail.imaps.minidletime", "5000");
  261. Session isession = Session.getInstance(props, null);
  262. // isession.setDebug(true);
  263. // adb -t 1 logcat | grep "eu.faircode.email\|System.out"
  264. istore = (IMAPStore) isession.getStore("imaps");
  265. final IMAPStore fstore = istore;
  266. // Listen for events
  267. istore.addStoreListener(new StoreListener() {
  268. @Override
  269. public void notification(StoreEvent e) {
  270. Log.i(Helper.TAG, account.name + " event: " + e.getMessage());
  271. // Check connection
  272. synchronized (state) {
  273. state.notifyAll();
  274. }
  275. }
  276. });
  277. istore.addFolderListener(new FolderAdapter() {
  278. @Override
  279. public void folderCreated(FolderEvent e) {
  280. // TODO: folder created
  281. }
  282. @Override
  283. public void folderRenamed(FolderEvent e) {
  284. // TODO: folder renamed
  285. }
  286. @Override
  287. public void folderDeleted(FolderEvent e) {
  288. // TODO: folder deleted
  289. }
  290. });
  291. // Listen for connection changes
  292. istore.addConnectionListener(new ConnectionAdapter() {
  293. List<Thread> folderThreads = new ArrayList<>();
  294. Map<Long, IMAPFolder> mapFolder = new HashMap<>();
  295. @Override
  296. public void opened(ConnectionEvent e) {
  297. Log.i(Helper.TAG, account.name + " opened");
  298. try {
  299. DB db = DB.getInstance(ServiceSynchronize.this);
  300. synchronizeFolders(account, fstore);
  301. for (final EntityFolder folder : db.folder().getFolders(account.id, true)) {
  302. Log.i(Helper.TAG, account.name + " sync folder " + folder.name);
  303. Thread thread = new Thread(new Runnable() {
  304. @Override
  305. public void run() {
  306. IMAPFolder ifolder = null;
  307. try {
  308. Log.i(Helper.TAG, folder.name + " start");
  309. ifolder = (IMAPFolder) fstore.getFolder(folder.name);
  310. ifolder.open(Folder.READ_WRITE);
  311. synchronized (mapFolder) {
  312. mapFolder.put(folder.id, ifolder);
  313. }
  314. monitorFolder(account, folder, fstore, ifolder);
  315. } catch (FolderNotFoundException ex) {
  316. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  317. } catch (Throwable ex) {
  318. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  319. reportError(account.name, folder.name, ex);
  320. // Cascade up
  321. try {
  322. fstore.close();
  323. } catch (MessagingException e1) {
  324. Log.w(Helper.TAG, account.name + " " + e1 + "\n" + Log.getStackTraceString(e1));
  325. }
  326. } finally {
  327. if (ifolder != null && ifolder.isOpen()) {
  328. try {
  329. ifolder.close(false);
  330. } catch (MessagingException ex) {
  331. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  332. }
  333. }
  334. Log.i(Helper.TAG, folder.name + " stop");
  335. }
  336. }
  337. }, "sync.folder." + folder.id);
  338. folderThreads.add(thread);
  339. thread.start();
  340. }
  341. IntentFilter f = new IntentFilter(ACTION_PROCESS_FOLDER);
  342. f.addDataType("account/" + account.id);
  343. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
  344. lbm.registerReceiver(processReceiver, f);
  345. Log.i(Helper.TAG, "listen process folder");
  346. for (final EntityFolder folder : db.folder().getFolders(account.id))
  347. if (!EntityFolder.TYPE_OUTBOX.equals(folder.type))
  348. lbm.sendBroadcast(new Intent(ACTION_PROCESS_FOLDER)
  349. .setType("account/" + account.id)
  350. .putExtra("folder", folder.id));
  351. } catch (Throwable ex) {
  352. Log.e(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  353. reportError(account.name, null, ex);
  354. // Cascade up
  355. try {
  356. fstore.close();
  357. } catch (MessagingException e1) {
  358. Log.w(Helper.TAG, account.name + " " + e1 + "\n" + Log.getStackTraceString(e1));
  359. }
  360. }
  361. }
  362. @Override
  363. public void disconnected(ConnectionEvent e) {
  364. Log.e(Helper.TAG, account.name + " disconnected");
  365. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
  366. lbm.unregisterReceiver(processReceiver);
  367. synchronized (mapFolder) {
  368. mapFolder.clear();
  369. }
  370. // Check connection
  371. synchronized (state) {
  372. state.notifyAll();
  373. }
  374. }
  375. @Override
  376. public void closed(ConnectionEvent e) {
  377. Log.e(Helper.TAG, account.name + " closed");
  378. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
  379. lbm.unregisterReceiver(processReceiver);
  380. synchronized (mapFolder) {
  381. mapFolder.clear();
  382. }
  383. // Check connection
  384. synchronized (state) {
  385. state.notifyAll();
  386. }
  387. }
  388. BroadcastReceiver processReceiver = new BroadcastReceiver() {
  389. @Override
  390. public void onReceive(Context context, Intent intent) {
  391. final long fid = intent.getLongExtra("folder", -1);
  392. IMAPFolder ifolder;
  393. synchronized (mapFolder) {
  394. ifolder = mapFolder.get(fid);
  395. }
  396. final boolean shouldClose = (ifolder == null);
  397. final IMAPFolder ffolder = ifolder;
  398. Log.i(Helper.TAG, "run operations folder=" + fid + " offline=" + shouldClose);
  399. executor.submit(new Runnable() {
  400. @Override
  401. public void run() {
  402. DB db = DB.getInstance(ServiceSynchronize.this);
  403. EntityFolder folder = db.folder().getFolder(fid);
  404. IMAPFolder ifolder = ffolder;
  405. try {
  406. Log.i(Helper.TAG, folder.name + " start operations");
  407. if (ifolder == null) {
  408. // Prevent unnecessary folder connections
  409. if (db.operation().getOperationCount(fid) == 0)
  410. return;
  411. ifolder = (IMAPFolder) fstore.getFolder(folder.name);
  412. ifolder.open(Folder.READ_WRITE);
  413. }
  414. processOperations(folder, fstore, ifolder);
  415. } catch (FolderNotFoundException ex) {
  416. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  417. } catch (Throwable ex) {
  418. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  419. reportError(account.name, folder.name, ex);
  420. } finally {
  421. if (shouldClose)
  422. if (ifolder != null && ifolder.isOpen()) {
  423. try {
  424. ifolder.close(false);
  425. } catch (MessagingException ex) {
  426. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  427. }
  428. }
  429. Log.i(Helper.TAG, folder.name + " stop operations");
  430. }
  431. }
  432. });
  433. }
  434. };
  435. });
  436. // Initiate connection
  437. Log.i(Helper.TAG, account.name + " connect");
  438. istore.connect(account.host, account.port, account.user, account.password);
  439. // Keep alive
  440. boolean connected = false;
  441. do {
  442. try {
  443. synchronized (state) {
  444. state.wait();
  445. }
  446. } catch (InterruptedException ex) {
  447. Log.w(Helper.TAG, account.name + " " + ex.toString());
  448. }
  449. if (state.running) {
  450. Log.i(Helper.TAG, account.name + " NOOP");
  451. connected = istore.isConnected();
  452. }
  453. } while (state.running && connected);
  454. if (state.running)
  455. Log.w(Helper.TAG, account.name + " not connected anymore");
  456. else
  457. Log.i(Helper.TAG, account.name + " not running anymore");
  458. } catch (Throwable ex) {
  459. Log.w(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  460. } finally {
  461. if (istore != null) {
  462. try {
  463. istore.close();
  464. } catch (MessagingException ex) {
  465. Log.w(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  466. }
  467. }
  468. }
  469. if (state.running) {
  470. try {
  471. Thread.sleep(10 * 1000L); // TODO: logarithmic back off
  472. } catch (InterruptedException ex) {
  473. Log.w(Helper.TAG, account.name + " " + ex.toString());
  474. }
  475. }
  476. }
  477. Log.i(Helper.TAG, account.name + " stopped");
  478. }
  479. private void monitorFolder(final EntityAccount account, final EntityFolder folder, final IMAPStore istore, final IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
  480. // Listen for new and deleted messages
  481. ifolder.addMessageCountListener(new MessageCountAdapter() {
  482. @Override
  483. public void messagesAdded(MessageCountEvent e) {
  484. try {
  485. Log.i(Helper.TAG, folder.name + " messages added");
  486. for (Message imessage : e.getMessages())
  487. synchronizeMessage(folder, ifolder, (IMAPMessage) imessage);
  488. } catch (MessageRemovedException ex) {
  489. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  490. } catch (Throwable ex) {
  491. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  492. reportError(account.name, folder.name, ex);
  493. // Cascade up
  494. try {
  495. istore.close();
  496. } catch (MessagingException e1) {
  497. Log.w(Helper.TAG, folder.name + " " + e1 + "\n" + Log.getStackTraceString(e1));
  498. }
  499. }
  500. }
  501. @Override
  502. public void messagesRemoved(MessageCountEvent e) {
  503. try {
  504. Log.i(Helper.TAG, folder.name + " messages removed");
  505. for (Message imessage : e.getMessages())
  506. try {
  507. long uid = ifolder.getUID(imessage);
  508. DB db = DB.getInstance(ServiceSynchronize.this);
  509. int count = db.message().deleteMessage(folder.id, uid);
  510. Log.i(Helper.TAG, "Deleted uid=" + uid + " count=" + count);
  511. } catch (MessageRemovedException ex) {
  512. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  513. }
  514. } catch (Throwable ex) {
  515. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  516. reportError(account.name, folder.name, ex);
  517. // Cascade up
  518. try {
  519. istore.close();
  520. } catch (MessagingException e1) {
  521. Log.w(Helper.TAG, folder.name + " " + e1 + "\n" + Log.getStackTraceString(e1));
  522. }
  523. }
  524. }
  525. });
  526. // Fetch e-mail
  527. synchronizeMessages(folder, ifolder);
  528. // Flags (like "seen") at the remote could be changed while synchronizing
  529. // Listen for changed messages
  530. ifolder.addMessageChangedListener(new MessageChangedListener() {
  531. @Override
  532. public void messageChanged(MessageChangedEvent e) {
  533. try {
  534. Log.i(Helper.TAG, folder.name + " message changed");
  535. synchronizeMessage(folder, ifolder, (IMAPMessage) e.getMessage());
  536. } catch (MessageRemovedException ex) {
  537. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  538. } catch (Throwable ex) {
  539. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  540. reportError(account.name, folder.name, ex);
  541. // Cascade up
  542. try {
  543. istore.close();
  544. } catch (MessagingException e1) {
  545. Log.w(Helper.TAG, folder.name + " " + e1 + "\n" + Log.getStackTraceString(e1));
  546. }
  547. }
  548. }
  549. });
  550. // Keep alive
  551. Log.i(Helper.TAG, folder.name + " start");
  552. try {
  553. Thread thread = new Thread(new Runnable() {
  554. @Override
  555. public void run() {
  556. try {
  557. boolean open;
  558. do {
  559. try {
  560. Thread.sleep(NOOP_INTERVAL);
  561. } catch (InterruptedException ex) {
  562. Log.w(Helper.TAG, folder.name + " " + ex.toString());
  563. }
  564. open = ifolder.isOpen();
  565. if (open)
  566. noop(folder, ifolder);
  567. } while (open);
  568. } catch (Throwable ex) {
  569. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  570. reportError(account.name, folder.name, ex);
  571. // Cascade up
  572. try {
  573. istore.close();
  574. } catch (MessagingException e1) {
  575. Log.w(Helper.TAG, folder.name + " " + e1 + "\n" + Log.getStackTraceString(e1));
  576. }
  577. }
  578. }
  579. }, "sync.noop." + folder.id);
  580. thread.start();
  581. // Idle
  582. while (state.running) {
  583. Log.i(Helper.TAG, folder.name + " start idle");
  584. ifolder.idle(false);
  585. Log.i(Helper.TAG, folder.name + " end idle");
  586. }
  587. } finally {
  588. Log.i(Helper.TAG, folder.name + " end");
  589. }
  590. }
  591. private void processOperations(EntityFolder folder, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
  592. try {
  593. Log.i(Helper.TAG, folder.name + " start process");
  594. DB db = DB.getInstance(this);
  595. DaoOperation operation = db.operation();
  596. DaoMessage message = db.message();
  597. for (TupleOperationEx op : operation.getOperations(folder.id))
  598. try {
  599. Log.i(Helper.TAG, folder.name +
  600. " start op=" + op.id + "/" + op.name +
  601. " args=" + op.args +
  602. " msg=" + op.message);
  603. JSONArray jargs = new JSONArray(op.args);
  604. try {
  605. if (EntityOperation.SEEN.equals(op.name)) {
  606. // Mark message (un)seen
  607. Message imessage = ifolder.getMessageByUID(op.uid);
  608. if (imessage == null)
  609. throw new MessageRemovedException();
  610. imessage.setFlag(Flags.Flag.SEEN, jargs.getBoolean(0));
  611. } else if (EntityOperation.ADD.equals(op.name)) {
  612. // Append message
  613. EntityMessage msg = message.getMessage(op.message);
  614. if (msg == null)
  615. return;
  616. // Disconnect from remote to prevent deletion
  617. Long uid = msg.uid;
  618. if (msg.uid != null) {
  619. msg.uid = null;
  620. message.updateMessage(msg);
  621. }
  622. // Execute append
  623. Properties props = MessageHelper.getSessionProperties();
  624. Session isession = Session.getInstance(props, null);
  625. MimeMessage imessage = MessageHelper.from(msg, isession);
  626. ifolder.appendMessages(new Message[]{imessage});
  627. // Drafts can be appended multiple times
  628. if (uid != null) {
  629. Message previously = ifolder.getMessageByUID(uid);
  630. if (previously == null)
  631. throw new MessageRemovedException();
  632. previously.setFlag(Flags.Flag.DELETED, true);
  633. ifolder.expunge();
  634. }
  635. } else if (EntityOperation.MOVE.equals(op.name)) {
  636. EntityFolder target = db.folder().getFolder(jargs.getLong(0));
  637. if (target == null)
  638. throw new FolderNotFoundException();
  639. // Move message
  640. Message imessage = ifolder.getMessageByUID(op.uid);
  641. if (imessage == null)
  642. throw new MessageRemovedException();
  643. Folder itarget = istore.getFolder(target.name);
  644. if (istore.hasCapability("MOVE"))
  645. ifolder.moveMessages(new Message[]{imessage}, itarget);
  646. else {
  647. Log.i(Helper.TAG, "MOVE by APPEND/DELETE");
  648. EntityMessage msg = message.getMessage(op.message);
  649. // Execute append
  650. Properties props = MessageHelper.getSessionProperties();
  651. Session isession = Session.getInstance(props, null);
  652. MimeMessage icopy = MessageHelper.from(msg, isession);
  653. itarget.appendMessages(new Message[]{icopy});
  654. // Execute delete
  655. imessage.setFlag(Flags.Flag.DELETED, true);
  656. ifolder.expunge();
  657. }
  658. message.deleteMessage(op.message);
  659. } else if (EntityOperation.DELETE.equals(op.name)) {
  660. // Delete message
  661. if (op.uid != null) {
  662. Message imessage = ifolder.getMessageByUID(op.uid);
  663. if (imessage == null)
  664. throw new MessageRemovedException();
  665. imessage.setFlag(Flags.Flag.DELETED, true);
  666. ifolder.expunge();
  667. }
  668. message.deleteMessage(op.message);
  669. } else if (EntityOperation.SEND.equals(op.name)) {
  670. // Send message
  671. EntityMessage msg = message.getMessage(op.message);
  672. if (msg == null)
  673. return;
  674. EntityMessage reply = (msg.replying == null ? null : message.getMessage(msg.replying));
  675. EntityIdentity ident = db.identity().getIdentity(msg.identity);
  676. if (ident == null || !ident.synchronize) {
  677. // Message will remain in outbox
  678. return;
  679. }
  680. // Create session
  681. Properties props = MessageHelper.getSessionProperties();
  682. Session isession = Session.getInstance(props, null);
  683. // Create message
  684. MimeMessage imessage;
  685. if (reply == null)
  686. imessage = MessageHelper.from(msg, isession);
  687. else
  688. imessage = MessageHelper.from(msg, reply, isession);
  689. if (ident.replyto != null)
  690. imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
  691. // Create transport
  692. Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps");
  693. try {
  694. // Connect transport
  695. itransport.connect(ident.host, ident.port, ident.user, ident.password);
  696. // Send message
  697. Address[] to = imessage.getAllRecipients();
  698. itransport.sendMessage(imessage, to);
  699. Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user +
  700. " to " + TextUtils.join(", ", to));
  701. msg.sent = new Date().getTime();
  702. msg.seen = true;
  703. msg.ui_seen = true;
  704. message.updateMessage(msg);
  705. // TODO: purge sent messages
  706. } finally {
  707. itransport.close();
  708. }
  709. // TODO: cache transport?
  710. } else if (EntityOperation.ATTACHMENT.equals(op.name)) {
  711. int sequence = jargs.getInt(0);
  712. EntityAttachment attachment = db.attachment().getAttachment(op.message, sequence);
  713. if (attachment == null)
  714. return;
  715. try {
  716. // Get message
  717. Message imessage = ifolder.getMessageByUID(op.uid);
  718. if (imessage == null)
  719. throw new MessageRemovedException();
  720. // Get attachment
  721. MessageHelper helper = new MessageHelper((MimeMessage) imessage);
  722. EntityAttachment a = helper.getAttachments().get(sequence - 1);
  723. // Download attachment
  724. InputStream is = a.part.getInputStream();
  725. ByteArrayOutputStream os = new ByteArrayOutputStream();
  726. byte[] buffer = new byte[DOWNLOAD_BUFFER_SIZE];
  727. for (int len = is.read(buffer); len != -1; len = is.read(buffer)) {
  728. os.write(buffer, 0, len);
  729. // Update progress
  730. if (attachment.size != null) {
  731. attachment.progress = os.size() * 100 / attachment.size;
  732. db.attachment().updateAttachment(attachment);
  733. Log.i(Helper.TAG, "Progress %=" + attachment.progress);
  734. }
  735. }
  736. // Store attachment data
  737. attachment.progress = null;
  738. attachment.content = os.toByteArray();
  739. db.attachment().updateAttachment(attachment);
  740. Log.i(Helper.TAG, "Downloaded bytes=" + attachment.content.length);
  741. } catch (Throwable ex) {
  742. // Reset progress on failure
  743. attachment.progress = null;
  744. db.attachment().updateAttachment(attachment);
  745. throw ex;
  746. }
  747. } else
  748. throw new MessagingException("Unknown operation name=" + op.name);
  749. // Operation succeeded
  750. operation.deleteOperation(op.id);
  751. } catch (MessageRemovedException ex) {
  752. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  753. // There is no use in repeating
  754. operation.deleteOperation(op.id);
  755. } catch (FolderNotFoundException ex) {
  756. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  757. // There is no use in repeating
  758. operation.deleteOperation(op.id);
  759. } catch (SMTPSendFailedException ex) {
  760. // Response codes: https://www.ietf.org/rfc/rfc821.txt
  761. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  762. // There is probably no use in repeating
  763. operation.deleteOperation(op.id);
  764. reportError(null, folder.name, ex);
  765. } catch (NullPointerException ex) {
  766. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  767. // There is no use in repeating
  768. operation.deleteOperation(op.id);
  769. reportError(null, folder.name, ex);
  770. }
  771. } finally {
  772. Log.i(Helper.TAG, folder.name + " end op=" + op.id + "/" + op.name);
  773. }
  774. } finally {
  775. Log.i(Helper.TAG, folder.name + " end process");
  776. }
  777. }
  778. private void synchronizeFolders(EntityAccount account, IMAPStore istore) throws MessagingException {
  779. try {
  780. Log.i(Helper.TAG, "Start sync folders");
  781. DaoFolder dao = DB.getInstance(this).folder();
  782. List<String> names = new ArrayList<>();
  783. for (EntityFolder folder : dao.getUserFolders(account.id))
  784. names.add(folder.name);
  785. Log.i(Helper.TAG, "Local folder count=" + names.size());
  786. Folder[] ifolders = istore.getDefaultFolder().list("*"); // TODO: is the pattern correct?
  787. Log.i(Helper.TAG, "Remote folder count=" + ifolders.length);
  788. for (Folder ifolder : ifolders) {
  789. String[] attrs = ((IMAPFolder) ifolder).getAttributes();
  790. boolean candidate = true;
  791. for (String attr : attrs) {
  792. if ("\\Noselect".equals(attr)) { // TODO: is this attribute correct?
  793. candidate = false;
  794. break;
  795. }
  796. if (attr.startsWith("\\"))
  797. if (EntityFolder.SYSTEM_FOLDER_ATTR.contains(attr.substring(1))) {
  798. candidate = false;
  799. break;
  800. }
  801. }
  802. if (candidate) {
  803. Log.i(Helper.TAG, ifolder.getFullName() + " candidate attr=" + TextUtils.join(",", attrs));
  804. EntityFolder folder = dao.getFolderByName(account.id, ifolder.getFullName());
  805. if (folder == null) {
  806. folder = new EntityFolder();
  807. folder.account = account.id;
  808. folder.name = ifolder.getFullName();
  809. folder.type = EntityFolder.TYPE_USER;
  810. folder.synchronize = false;
  811. folder.after = 0;
  812. dao.insertFolder(folder);
  813. Log.i(Helper.TAG, folder.name + " added");
  814. } else
  815. names.remove(folder.name);
  816. }
  817. }
  818. Log.i(Helper.TAG, "Delete local folder=" + names.size());
  819. for (String name : names)
  820. dao.deleteFolder(account.id, name);
  821. } finally {
  822. Log.i(Helper.TAG, "End sync folder");
  823. }
  824. }
  825. private void synchronizeMessages(EntityFolder folder, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
  826. try {
  827. Log.i(Helper.TAG, folder.name + " start sync after=" + folder.after);
  828. DB db = DB.getInstance(this);
  829. DaoMessage dao = db.message();
  830. // Get reference times
  831. Calendar cal = Calendar.getInstance();
  832. cal.add(Calendar.DAY_OF_MONTH, -folder.after);
  833. cal.set(Calendar.HOUR_OF_DAY, 0);
  834. cal.set(Calendar.MINUTE, 0);
  835. cal.set(Calendar.SECOND, 0);
  836. cal.set(Calendar.MILLISECOND, 0);
  837. long ago = cal.getTimeInMillis();
  838. Log.i(Helper.TAG, folder.name + " ago=" + new Date(ago));
  839. // Delete old local messages
  840. int old = dao.deleteMessagesBefore(folder.id, ago);
  841. Log.i(Helper.TAG, folder.name + " local old=" + old);
  842. // Get list of local uids
  843. List<Long> uids = dao.getUids(folder.id, ago);
  844. Log.i(Helper.TAG, folder.name + " local count=" + uids.size());
  845. // Reduce list of local uids
  846. long search = SystemClock.elapsedRealtime();
  847. Message[] imessages = ifolder.search(new ReceivedDateTerm(ComparisonTerm.GE, new Date(ago)));
  848. Log.i(Helper.TAG, folder.name + " remote count=" + imessages.length +
  849. " search=" + (SystemClock.elapsedRealtime() - search) + " ms");
  850. FetchProfile fp = new FetchProfile();
  851. fp.add(UIDFolder.FetchProfileItem.UID);
  852. fp.add(IMAPFolder.FetchProfileItem.FLAGS);
  853. ifolder.fetch(imessages, fp);
  854. long fetch = SystemClock.elapsedRealtime();
  855. Log.i(Helper.TAG, folder.name + " remote fetched=" + (SystemClock.elapsedRealtime() - fetch) + " ms");
  856. for (Message imessage : imessages)
  857. try {
  858. uids.remove(ifolder.getUID(imessage));
  859. } catch (MessageRemovedException ex) {
  860. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  861. }
  862. // Delete local messages not at remote
  863. Log.i(Helper.TAG, folder.name + " delete=" + uids.size());
  864. for (Long uid : uids) {
  865. int count = dao.deleteMessage(folder.id, uid);
  866. Log.i(Helper.TAG, folder.name + " delete local uid=" + uid + " count=" + count);
  867. }
  868. // Add/update local messages
  869. Log.i(Helper.TAG, folder.name + " add=" + imessages.length);
  870. for (int batch = 0; batch < imessages.length; batch += FETCH_BATCH_SIZE) {
  871. Log.i(Helper.TAG, folder.name + " fetch @" + batch);
  872. try {
  873. db.beginTransaction();
  874. for (int i = 0; i < FETCH_BATCH_SIZE && batch + i < imessages.length; i++)
  875. try {
  876. synchronizeMessage(folder, ifolder, (IMAPMessage) imessages[batch + i]);
  877. } catch (MessageRemovedException ex) {
  878. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  879. }
  880. db.setTransactionSuccessful();
  881. } finally {
  882. db.endTransaction();
  883. }
  884. }
  885. } finally {
  886. Log.i(Helper.TAG, folder.name + " end sync");
  887. }
  888. }
  889. private void synchronizeMessage(EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage) throws MessagingException, JSONException, IOException {
  890. long uid = -1;
  891. try {
  892. FetchProfile fp = new FetchProfile();
  893. fp.add(UIDFolder.FetchProfileItem.UID);
  894. fp.add(IMAPFolder.FetchProfileItem.FLAGS);
  895. ifolder.fetch(new Message[]{imessage}, fp);
  896. uid = ifolder.getUID(imessage);
  897. Log.i(Helper.TAG, folder.name + " start sync uid=" + uid);
  898. if (imessage.isExpunged()) {
  899. Log.i(Helper.TAG, folder.name + " expunged uid=" + uid);
  900. return;
  901. }
  902. if (imessage.isSet(Flags.Flag.DELETED)) {
  903. Log.i(Helper.TAG, folder.name + " deleted uid=" + uid);
  904. return;
  905. }
  906. MessageHelper helper = new MessageHelper(imessage);
  907. boolean seen = helper.getSeen();
  908. DB db = DB.getInstance(this);
  909. EntityMessage message = db.message().getMessage(folder.id, uid);
  910. if (message == null) {
  911. FetchProfile fp1 = new FetchProfile();
  912. fp1.add(FetchProfile.Item.ENVELOPE);
  913. fp1.add(FetchProfile.Item.CONTENT_INFO);
  914. fp1.add(IMAPFolder.FetchProfileItem.HEADERS);
  915. fp1.add(IMAPFolder.FetchProfileItem.MESSAGE);
  916. ifolder.fetch(new Message[]{imessage}, fp1);
  917. long id = MimeMessageEx.getId(imessage);
  918. message = db.message().getMessage(id);
  919. if (message != null && message.folder != folder.id) {
  920. if (EntityFolder.TYPE_ARCHIVE.equals(folder.type))
  921. message = null;
  922. else // Outbox to sent
  923. message.folder = folder.id;
  924. }
  925. boolean update = (message != null);
  926. if (message == null)
  927. message = new EntityMessage();
  928. message.account = folder.account;
  929. message.folder = folder.id;
  930. message.uid = uid;
  931. message.msgid = helper.getMessageID();
  932. message.references = TextUtils.join(" ", helper.getReferences());
  933. message.inreplyto = helper.getInReplyTo();
  934. message.thread = helper.getThreadId(uid);
  935. message.from = helper.getFrom();
  936. message.to = helper.getTo();
  937. message.cc = helper.getCc();
  938. message.bcc = helper.getBcc();
  939. message.reply = helper.getReply();
  940. message.subject = imessage.getSubject();
  941. message.body = helper.getHtml();
  942. message.received = imessage.getReceivedDate().getTime();
  943. message.sent = (imessage.getSentDate() == null ? null : imessage.getSentDate().getTime());
  944. message.seen = seen;
  945. message.ui_seen = seen;
  946. message.ui_hide = false;
  947. if (update) {
  948. db.message().updateMessage(message);
  949. Log.i(Helper.TAG, folder.name + " updated id=" + message.id + " uid=" + message.uid);
  950. } else {
  951. message.id = db.message().insertMessage(message);
  952. Log.i(Helper.TAG, folder.name + " added id=" + message.id + " uid=" + message.uid);
  953. }
  954. int sequence = 0;
  955. for (EntityAttachment attachment : helper.getAttachments()) {
  956. sequence++;
  957. Log.i(Helper.TAG, "attachment seq=" + sequence +
  958. " name=" + attachment.name + " type=" + attachment.type);
  959. attachment.message = message.id;
  960. attachment.sequence = sequence;
  961. attachment.id = db.attachment().insertAttachment(attachment);
  962. }
  963. } else if (message.seen != seen) {
  964. message.seen = seen;
  965. message.ui_seen = seen;
  966. // TODO: synchronize all data?
  967. db.message().updateMessage(message);
  968. Log.i(Helper.TAG, folder.name + " updated id=" + message.id + " uid=" + message.uid);
  969. } else {
  970. Log.i(Helper.TAG, folder.name + " unchanged id=" + message.id + " uid=" + message.uid);
  971. }
  972. } finally {
  973. Log.i(Helper.TAG, folder.name + " end sync uid=" + uid);
  974. }
  975. }
  976. private void noop(EntityFolder folder, final IMAPFolder ifolder) throws MessagingException {
  977. Log.i(Helper.TAG, folder.name + " request NOOP");
  978. ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
  979. public Object doCommand(IMAPProtocol p) throws ProtocolException {
  980. Log.i(Helper.TAG, ifolder.getName() + " start NOOP");
  981. p.simpleCommand("NOOP", null);
  982. Log.i(Helper.TAG, ifolder.getName() + " end NOOP");
  983. return null;
  984. }
  985. });
  986. }
  987. ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
  988. private Thread mainThread;
  989. private EntityFolder outbox = null;
  990. @Override
  991. public void onAvailable(Network network) {
  992. Log.i(Helper.TAG, "Available " + network);
  993. synchronized (state) {
  994. if (!state.running) {
  995. state.threads.clear();
  996. state.running = true;
  997. mainThread = new Thread(new Runnable() {
  998. @Override
  999. public void run() {
  1000. DB db = DB.getInstance(ServiceSynchronize.this);
  1001. try {
  1002. List<EntityAccount> accounts = db.account().getAccounts(true);
  1003. if (accounts.size() == 0) {
  1004. Log.i(Helper.TAG, "No accounts, halt");
  1005. stopSelf();
  1006. } else
  1007. for (final EntityAccount account : accounts) {
  1008. Log.i(Helper.TAG, account.host + "/" + account.user + " run");
  1009. Thread thread = new Thread(new Runnable() {
  1010. @Override
  1011. public void run() {
  1012. try {
  1013. monitorAccount(account);
  1014. } catch (Throwable ex) {
  1015. // Fallsafe
  1016. Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  1017. }
  1018. }
  1019. }, "sync.account." + account.id);
  1020. state.threads.add(thread);
  1021. thread.start();
  1022. }
  1023. } catch (Throwable ex) {
  1024. // Failsafe
  1025. Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  1026. }
  1027. outbox = db.folder().getOutbox();
  1028. if (outbox != null) {
  1029. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
  1030. lbm.registerReceiver(receiverOutbox, new IntentFilter(ACTION_PROCESS_OUTBOX));
  1031. Log.i(Helper.TAG, outbox.name + " listen operations");
  1032. lbm.sendBroadcast(new Intent(ACTION_PROCESS_OUTBOX));
  1033. }
  1034. }
  1035. }, "sync.main");
  1036. mainThread.start();
  1037. }
  1038. }
  1039. }
  1040. @Override
  1041. public void onLost(Network network) {
  1042. Log.i(Helper.TAG, "Lost " + network);
  1043. synchronized (state) {
  1044. if (state.running) {
  1045. state.running = false;
  1046. state.notifyAll();
  1047. }
  1048. }
  1049. if (outbox != null) {
  1050. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
  1051. lbm.unregisterReceiver(receiverOutbox);
  1052. Log.i(Helper.TAG, outbox.name + " unlisten operations");
  1053. }
  1054. }
  1055. BroadcastReceiver receiverOutbox = new BroadcastReceiver() {
  1056. @Override
  1057. public void onReceive(Context context, Intent intent) {
  1058. Log.i(Helper.TAG, outbox.name + " run operations");
  1059. executor.submit(new Runnable() {
  1060. @Override
  1061. public void run() {
  1062. try {
  1063. Log.i(Helper.TAG, outbox.name + " start operations");
  1064. synchronized (outbox) {
  1065. processOperations(outbox, null, null);
  1066. }
  1067. } catch (Throwable ex) {
  1068. Log.e(Helper.TAG, outbox.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  1069. reportError(null, outbox.name, ex);
  1070. } finally {
  1071. Log.i(Helper.TAG, outbox.name + " end operations");
  1072. }
  1073. }
  1074. });
  1075. }
  1076. };
  1077. };
  1078. public static void start(Context context) {
  1079. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
  1080. NotificationManager nm = context.getSystemService(NotificationManager.class);
  1081. NotificationChannel service = new NotificationChannel(
  1082. "service",
  1083. context.getString(R.string.channel_service),
  1084. NotificationManager.IMPORTANCE_MIN);
  1085. service.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT);
  1086. nm.createNotificationChannel(service);
  1087. NotificationChannel notification = new NotificationChannel(
  1088. "notification",
  1089. context.getString(R.string.channel_notification),
  1090. NotificationManager.IMPORTANCE_DEFAULT);
  1091. nm.createNotificationChannel(notification);
  1092. NotificationChannel error = new NotificationChannel(
  1093. "error",
  1094. context.getString(R.string.channel_error),
  1095. NotificationManager.IMPORTANCE_HIGH);
  1096. nm.createNotificationChannel(error);
  1097. }
  1098. ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class));
  1099. }
  1100. public static void stop(Context context, String reason) {
  1101. Log.i(Helper.TAG, "Stop because of '" + reason + "'");
  1102. context.stopService(new Intent(context, ServiceSynchronize.class));
  1103. }
  1104. public static void restart(Context context, String reason) {
  1105. Log.i(Helper.TAG, "Restart because of '" + reason + "'");
  1106. context.stopService(new Intent(context, ServiceSynchronize.class));
  1107. start(context);
  1108. }
  1109. }