/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package nya.miku.wishmaster.ui.tabs; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.LockSupport; import org.apache.commons.lang3.tuple.Triple; import nya.miku.wishmaster.R; import nya.miku.wishmaster.api.ChanModule; import nya.miku.wishmaster.api.interfaces.CancellableTask; import nya.miku.wishmaster.api.interfaces.CancellableTask.BaseCancellableTask; import nya.miku.wishmaster.api.models.UrlPageModel; import nya.miku.wishmaster.api.util.PageLoaderFromChan; import nya.miku.wishmaster.cache.PagesCache; import nya.miku.wishmaster.cache.SerializablePage; import nya.miku.wishmaster.common.Async; import nya.miku.wishmaster.common.Logger; import nya.miku.wishmaster.common.MainApplication; import nya.miku.wishmaster.http.interactive.InteractiveException; import nya.miku.wishmaster.ui.MainActivity; import nya.miku.wishmaster.ui.downloading.BackgroundThumbDownloader; import nya.miku.wishmaster.ui.presentation.BoardFragment; import nya.miku.wishmaster.ui.presentation.PresentationModel; import nya.miku.wishmaster.ui.settings.ApplicationSettings; import nya.miku.wishmaster.ui.settings.Wifi; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Build; import android.os.IBinder; import android.support.v4.app.NotificationCompat; /** * Сервис автообновления * @author miku-nyan * */ public class TabsTrackerService extends Service { private static final String TAG = "TabsTrackerService"; public static final String EXTRA_UPDATE_IMMEDIATELY = "UpdateImmediately"; public static final String EXTRA_CLEAR_SUBSCRIPTIONS = "ClearSubscriptions"; public static final String BROADCAST_ACTION_NOTIFY = "nya.miku.wishmaster.BROADCAST_ACTION_TRACKER_NOTIFY"; public static final String BROADCAST_ACTION_CLEAR_SUBSCRIPTIONS = "nya.miku.wishmaster.BROADCAST_ACTION_CLEAR_SUBSCRIPTIONS"; public static final int TRACKER_NOTIFICATION_UPDATE_ID = 40; public static final int TRACKER_NOTIFICATION_SUBSCRIPTIONS_ID = 50; /** true, если сервис сейчас работает */ private static boolean running = false; /** если true, в заголовке уведомления будет написано "есть новые сообщения" */ private static boolean unread = false; /** если true, выведется уведомление об ответе на отслеживаемые посты */ private static boolean subscriptions = false; /** список тредов, в которых есть ответы на отслеживаемые посты (triple: url вкладки, url со ссылкой на пост, заголовок вкладки) */ private static List<Triple<String, String, String>> subscriptionsData = null; /** ID вкладки, которая обновляется в данный момент или -1 */ private static long currentUpdatingTabId = -1; /** true, если сервис сейчас работает */ public static boolean isRunning() { return running; } /** добавить тред, в котором есть ответы на отслеживаемые посты (будет выведено уведомление) */ public static void addSubscriptionNotification(String tabUrl, String postNumber, String tabTitle) { List<Triple<String, String, String>> list = subscriptionsData; if (list == null) list = new ArrayList<>(); int index = findTab(list, tabUrl, tabTitle); if (index == -1) { String postUrl = tabUrl; try { UrlPageModel pageModel = UrlHandler.getPageModel(tabUrl); if (pageModel != null) { pageModel.postNumber = postNumber; postUrl = MainApplication.getInstance().getChanModule(pageModel.chanName).buildUrl(pageModel); } } catch (Exception e) { Logger.e(TAG, e); } list.add(Triple.of(tabUrl, postUrl, tabTitle)); } else { String postUrl = list.get(index).getMiddle(); list.set(index, Triple.of(tabUrl, postUrl, tabTitle)); } subscriptionsData = list; subscriptions = true; } /** установить флаг непрочитанных сообщений: в заголовке уведомления об автообновлении будет написано "есть новые сообщения" */ public static void setUnread() { unread = true; } /** очистить состояние уведомления об автообновлении: убрать надпись "есть новые сообщения" */ public static void clearUnread() { unread = false; } /** очистить список тредов, в которых есть ответы на отслеживаемые посты, в уведомлении об отслеживаемых * (при этом, если уведомление об отслеживаемых на данный момент не было создано, оно не будет создано) */ public static void clearSubscriptions() { subscriptions = false; subscriptionsData = null; } /** получить ID вкладки, которая обновляется в данный момент; вернёт -1, если обновление не выполняется в данный момент */ public static long getCurrentUpdatingTabId() { return currentUpdatingTabId; } /** вызывается, когда открыта вкладка; если уведомление об отслеживаемых ссылается на эту вкладку, оно будет отменено */ public static void onResumeTab(Context context, String tabUrl, String tabTitle) { List<Triple<String, String, String>> list = subscriptionsData; if (list == null) return; int index = findTab(list, tabUrl, tabTitle); if (index != -1) { ((NotificationManager) context.getSystemService(NOTIFICATION_SERVICE)).cancel(TRACKER_NOTIFICATION_SUBSCRIPTIONS_ID); clearSubscriptions(); } } private static int findTab(List<Triple<String, String, String>> list, String tabUrl, String tabTitle) { for (int i=0; i<list.size(); ++i) { Triple<String, String, String> triple = list.get(i); if (tabUrl == null) { if (triple.getLeft() == null && tabTitle.equals(triple.getRight())) { return i; } } else { if (tabUrl.equals(triple.getLeft())) { return i; } } } return -1; } private ApplicationSettings settings; private TabsState tabsState; private TabsSwitcher tabsSwitcher; private PagesCache pagesCache; private NotificationManager notificationManager; private BroadcastReceiver broadcastReceiver; private boolean isForeground = false; private int timerDelay; private boolean enableNotification; private boolean backgroundTabs; private boolean immediately = false; private CancellableTask task = null; private void notifyForeground(int id, Notification notification) { if (!isForeground) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ECLAIR) { try { getClass().getMethod("setForeground", new Class[] { boolean.class }).invoke(this, Boolean.TRUE); } catch (Exception e) { Logger.e(TAG, "cannot invoke setForeground(true)", e); } notificationManager.notify(id, notification); } else { ForegroundCompat.startForeground(this, id, notification); } isForeground = true; } else { notificationManager.notify(id, notification); } } private void cancelForeground(int id) { if (isForeground) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ECLAIR) { notificationManager.cancel(id); try { getClass().getMethod("setForeground", new Class[] { boolean.class }).invoke(this, Boolean.FALSE); } catch (Exception e) { Logger.e(TAG, "cannot invoke setForeground(false)", e); } } else { ForegroundCompat.stopForeground(this); } isForeground = false; } else { notificationManager.cancel(id); } } @TargetApi(Build.VERSION_CODES.ECLAIR) private static class ForegroundCompat { static void startForeground(Service service, int id, Notification notification) { service.startForeground(id, notification); } static void stopForeground(Service service) { service.stopForeground(true); } } @Override public void onCreate() { Logger.d(TAG, "TabsTrackerService creating"); super.onCreate(); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); settings = MainApplication.getInstance().settings; tabsState = MainApplication.getInstance().tabsState; tabsSwitcher = MainApplication.getInstance().tabsSwitcher; pagesCache = MainApplication.getInstance().pagesCache; registerReceiver(broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Logger.d(TAG, "received BROADCAST_ACTION_CLEAR_SUBSCRIPTIONS"); clearSubscriptions(); } }, new IntentFilter(BROADCAST_ACTION_CLEAR_SUBSCRIPTIONS)); } @SuppressLint("InlinedApi") @Override public int onStartCommand(Intent intent, int flags, int startId) { onStart(intent, startId); return Service.START_STICKY; } @Override public void onStart(Intent intent, int startId) { Logger.d(TAG, "TabsTrackerService starting"); enableNotification = settings.isAutoupdateNotification(); immediately = intent != null && intent.getBooleanExtra(EXTRA_UPDATE_IMMEDIATELY, false); backgroundTabs = settings.isAutoupdateBackground(); timerDelay = settings.getAutoupdateDelay(); if (running) { Logger.d(TAG, "TabsTrackerService service already running"); return; } clearUnread(); clearSubscriptions(); TrackerLoop loop = new TrackerLoop(); task = loop; Async.runAsync(loop); running = true; } private void doUpdate(final CancellableTask task) { if (backgroundTabs || immediately) { int tabsArrayLength = tabsState.tabsArray.size(); TabModel[] tabsArray = new TabModel[tabsArrayLength]; //avoid of java.util.ConcurrentModificationException for (int i=0; i<tabsArrayLength; ++i) tabsArray[i] = tabsState.tabsArray.get(i); for (final TabModel tab : tabsArray) { if (task.isCancelled()) return; if (settings.isAutoupdateWifiOnly() && !Wifi.isConnected() && !immediately) return; if (tab.type == TabModel.TYPE_NORMAL && tab.pageModel.type == UrlPageModel.TYPE_THREADPAGE && tab.autoupdateBackground) { if (tabsSwitcher.currentId != null && tabsSwitcher.currentId.equals(tab.id)) continue; final String hash = tab.hash; ChanModule chan = MainApplication.getInstance().getChanModule(tab.pageModel.chanName); currentUpdatingTabId = tab.id; final PresentationModel presentationModel = pagesCache.getPresentationModel(hash); final SerializablePage serializablePage; if (presentationModel != null) { serializablePage = presentationModel.source; } else { SerializablePage pageFromFilecache = pagesCache.getSerializablePage(hash); if (pageFromFilecache != null) { serializablePage = pageFromFilecache; } else { serializablePage = new SerializablePage(); serializablePage.pageModel = tab.pageModel; } } final int oldCount = serializablePage.posts != null ? serializablePage.posts.length : 0; new PageLoaderFromChan(serializablePage, new PageLoaderFromChan.PageLoaderCallback() { @Override public void onSuccess() { BackgroundThumbDownloader.download(serializablePage, task); MainApplication.getInstance().subscriptions.checkOwnPost(serializablePage, oldCount); tab.autoupdateError = false; int newCount = serializablePage.posts != null ? serializablePage.posts.length : 0; if (oldCount != newCount) { if (oldCount != 0) tab.unreadPostsCount += (newCount - oldCount); setUnread(); int checkSubscriptions = MainApplication.getInstance().subscriptions.checkSubscriptions(serializablePage, oldCount); if (checkSubscriptions >= 0) { addSubscriptionNotification(tab.webUrl, serializablePage.posts[checkSubscriptions].number, tab.title); tab.unreadSubscriptions = true; } } if (presentationModel != null) { presentationModel.setNotReady(); pagesCache.putPresentationModel(hash, presentationModel); } else { pagesCache.putSerializablePage(hash, serializablePage); } } @Override public void onInteractiveException(InteractiveException e) { tab.autoupdateError = true; } @Override public void onError(String message) { tab.autoupdateError = true; } }, chan, task).run(); } } currentUpdatingTabId = -1; } if (task.isCancelled()) return; if (settings.isAutoupdateWifiOnly() && !Wifi.isConnected() && !immediately) return; if (tabsSwitcher.currentFragment instanceof BoardFragment) { TabModel tab = tabsState.findTabById(tabsSwitcher.currentId); if (tab != null && tab.pageModel != null && tab.type == TabModel.TYPE_NORMAL && tab.pageModel.type == UrlPageModel.TYPE_THREADPAGE) { Async.runOnUiThread(new Runnable() { @Override public void run() { try { ((BoardFragment) tabsSwitcher.currentFragment).updateSilent(); } catch (Exception e) { Logger.e(TAG, e); } } }); } } } private class TrackerLoop extends BaseCancellableTask implements Runnable { private int timerCounter = 0; @Override public void run() { while (true) { if (isCancelled()) { cancelForeground(TRACKER_NOTIFICATION_UPDATE_ID); return; } Notification subscriptionsNotification = getSubscriptionsNotification(); if (subscriptionsNotification != null) notificationManager.notify(TRACKER_NOTIFICATION_SUBSCRIPTIONS_ID, subscriptionsNotification); if (++timerCounter > timerDelay || immediately) { timerCounter = 0; if (enableNotification) { notifyForeground(TRACKER_NOTIFICATION_UPDATE_ID, getUpdateNotification(-1)); } if (!settings.isAutoupdateWifiOnly() || Wifi.isConnected() || immediately) { doUpdate(this); immediately = false; } if (isCancelled()) { cancelForeground(TRACKER_NOTIFICATION_UPDATE_ID); return; } else { sendBroadcast(new Intent(BROADCAST_ACTION_NOTIFY)); } if (!settings.isAutoupdateEnabled()) stopSelf(); } else { if (enableNotification) { int remainingTime = timerDelay - timerCounter + 1; notifyForeground(TRACKER_NOTIFICATION_UPDATE_ID, getUpdateNotification(remainingTime)); } } LockSupport.parkNanos(1000000000); } } //если secondsRemaining == -1, текст будет "выполняется обновление" private Notification getUpdateNotification(int secondsRemaining) { return notifUpdate. setContentTitle(getString(unread ? R.string.tabs_tracker_title_unread : R.string.tabs_tracker_title)). setContentText(secondsRemaining == -1 ? getString(R.string.tabs_tracker_updating) : getResources().getQuantityString(R.plurals.tabs_tracker_timer, secondsRemaining, secondsRemaining)). build(); } private Notification getSubscriptionsNotification() { if (!subscriptions) return null; subscriptions = false; List<Triple<String, String, String>> list = subscriptionsData; if (list == null || list.size() == 0) return null; String url = list.get(0).getMiddle(); Intent activityIntent = new Intent(TabsTrackerService.this, MainActivity.class).putExtra(EXTRA_CLEAR_SUBSCRIPTIONS, true); if (url != null) activityIntent.setData(Uri.parse(url)); NotificationCompat.InboxStyle style = list.size() == 1 ? null : new NotificationCompat.InboxStyle(). addLine(getString(R.string.subscriptions_notification_text_format, list.get(0).getRight())). addLine(getString(R.string.subscriptions_notification_text_format, list.get(1).getRight())); if (list.size() > 2) style.setSummaryText(getString(R.string.subscriptions_notification_text_more, list.size() - 2)); return notifSubscription. setContentText(list.size() > 1 ? getString(R.string.subscriptions_notification_text_multiple) : getString(R.string.subscriptions_notification_text_format, list.get(0).getRight())). setStyle(style). setContentIntent(PendingIntent.getActivity(TabsTrackerService.this, 0, activityIntent, PendingIntent.FLAG_CANCEL_CURRENT)). build(); } private NotificationCompat.Builder notifUpdate = new NotificationCompat.Builder(TabsTrackerService.this). setSmallIcon(R.drawable.ic_launcher). setCategory(NotificationCompat.CATEGORY_SERVICE). setContentIntent(PendingIntent.getActivity( TabsTrackerService.this, 0, new Intent(TabsTrackerService.this, MainActivity.class), PendingIntent.FLAG_CANCEL_CURRENT)); private NotificationCompat.Builder notifSubscription = new NotificationCompat.Builder(TabsTrackerService.this). setSmallIcon(R.drawable.ic_launcher). setDefaults(NotificationCompat.DEFAULT_ALL). setOngoing(false). setAutoCancel(true). setOnlyAlertOnce(true). setCategory(NotificationCompat.CATEGORY_MESSAGE). setContentTitle(getString(R.string.subscriptions_notification_title)). setDeleteIntent(PendingIntent.getBroadcast( TabsTrackerService.this, 0, new Intent(BROADCAST_ACTION_CLEAR_SUBSCRIPTIONS), PendingIntent.FLAG_CANCEL_CURRENT)); } @Override public void onDestroy() { Logger.d(TAG, "TabsTrackerService destroying"); if (task != null) task.cancel(); running = false; unregisterReceiver(broadcastReceiver); } @Override public IBinder onBind(Intent intent) { return null; } }