list = helper.pinDao.queryForAll();
+ for (int i = 0; i < list.size(); i++) {
+ Pin p = list.get(i);
+ helper.loadableDao.refresh(p.loadable);
+ }
+ return list;
+ }
+ };
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
index 51735c8d..49725e04 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
@@ -17,34 +17,89 @@
*/
package org.floens.chan.core.manager;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+
+import com.android.volley.VolleyError;
import org.floens.chan.Chan;
+import org.floens.chan.chan.ChanLoader;
+import org.floens.chan.core.database.DatabaseManager;
+import org.floens.chan.core.database.DatabasePinManager;
+import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post;
-import org.floens.chan.core.pool.LoadablePool;
+import org.floens.chan.core.pool.LoaderPool;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.helper.PostHelper;
import org.floens.chan.ui.service.WatchNotifier;
-import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
import de.greenrobot.event.EventBus;
+import static org.floens.chan.utils.AndroidUtils.getAppContext;
+
+/**
+ * Manages all Pin related management.
+ *
+ * Pins are threads that are pinned to a pane on the left.
+ *
+ * The pin watcher is an optional feature that watches threads for new posts and displays a new
+ * post counter next to the pin view. Watching happens with the same backoff timer as used for
+ * the auto updater for open threads.
+ *
+ * Background watching is a feature that can be enabled. With background watching enabled then
+ * the PinManager will register an AlarmManager to check for updates in intervals. It will acquire
+ * a wakelock shortly while checking for updates.
+ *
+ * All pin adding and removing must go through this class to properly update the watchers.
+ */
public class WatchManager {
private static final String TAG = "WatchManager";
- private static final int FOREGROUND_TIME = 5;
+
+ private enum IntervalType {
+ /**
+ * A timer that uses a {@link Handler} that calls {@link #update(boolean)} every {@value #FOREGROUND_INTERVAL}ms.
+ */
+ FOREGROUND,
+
+ /**
+ * A timer that schedules a broadcast to be send that calls {@link #update(boolean)}.
+ */
+ BACKGROUND,
+
+ /**
+ * No scheduling.
+ */
+ NONE
+ }
+
+ public static final int DEFAULT_BACKGROUND_INTERVAL = 15 * 60 * 1000;
+
+ private static final long FOREGROUND_INTERVAL = 15 * 1000;
+ private static final int MESSAGE_UPDATE = 1;
+ private static final int REQUEST_CODE_WATCH_UPDATE = 2;
+ private static final String WATCHER_UPDATE_ACTION = "org.floens.chan.intent.action.WATCHER_UPDATE";
+ private static final String WAKELOCK_TAG = "WatchManagerUpdateLock";
+ private static final long WAKELOCK_MAX_TIME = 60 * 1000;
+ private static final long BACKGROUND_UPDATE_MIN_DELAY = 90 * 1000;
private static final Comparator SORT_PINS = new Comparator() {
@Override
@@ -53,74 +108,61 @@ public class WatchManager {
}
};
- private final Context context;
+ private final AlarmManager alarmManager;
+ private final PowerManager powerManager;
+
private final List pins;
- private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
- private PendingTimer pendingTimer;
+ private final DatabaseManager databaseManager;
+ private final DatabasePinManager databasePinManager;
- public WatchManager(Context context) {
- this.context = context;
+ private final Handler handler;
- pins = Chan.getDatabaseManager().getPinned();
- Collections.sort(pins, SORT_PINS);
+ private IntervalType currentInterval = IntervalType.NONE;
- EventBus.getDefault().register(this);
+ private Map pinWatchers = new HashMap<>();
- updateTimerState(true);
- updateNotificationServiceState();
- updatePinWatchers();
- }
+ private Set waitingForPinWatchersForBackgroundUpdate;
+ private PowerManager.WakeLock wakeLock;
+ private long lastBackgroundUpdateTime;
- /**
- * Look for a pin that has an loadable that is equal to the supplied
- * loadable.
- *
- * @param other
- * @return The pin whose loadable is equal to the supplied loadable, or null
- * if no pin was found.
- */
- public Pin findPinByLoadable(Loadable other) {
- for (Pin pin : pins) {
- if (pin.loadable.equals(other)) {
- return pin;
- }
- }
+ public WatchManager() {
+ alarmManager = (AlarmManager) getAppContext().getSystemService(Context.ALARM_SERVICE);
+ powerManager = (PowerManager) getAppContext().getSystemService(Context.POWER_SERVICE);
- return null;
- }
+ databaseManager = Chan.getDatabaseManager();
+ databasePinManager = databaseManager.getDatabasePinManager();
+ pins = databaseManager.runTaskSync(databasePinManager.getPins());
+ Collections.sort(pins, SORT_PINS);
- public Pin findPinById(int id) {
- for (Pin pin : pins) {
- if (pin.id == id) {
- return pin;
+ handler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MESSAGE_UPDATE) {
+ update(false);
+ return true;
+ } else {
+ return false;
+ }
}
- }
+ });
- return null;
- }
+ EventBus.getDefault().register(this);
- public List getPins() {
- return pins;
+ updateState();
}
- public List getWatchingPins() {
- if (ChanSettings.watchEnabled.get()) {
- List l = new ArrayList<>();
-
- for (Pin p : pins) {
- if (p.watching)
- l.add(p);
- }
-
- return l;
- } else {
- return Collections.emptyList();
- }
+ public boolean createPin(Loadable loadable, Post opPost) {
+ Pin pin = new Pin();
+ pin.loadable = loadable;
+ pin.loadable.title = PostHelper.getTitle(opPost, loadable);
+ pin.thumbnailUrl = opPost.thumbnailUrl;
+ return createPin(pin);
}
- public boolean addPin(Pin pin) {
+ public boolean createPin(Pin pin) {
// No duplicates
- for (Pin e : pins) {
+ for (int i = 0; i < pins.size(); i++) {
+ Pin e = pins.get(i);
if (e.loadable.equals(pin.loadable)) {
return false;
}
@@ -128,233 +170,377 @@ public class WatchManager {
pin.order = pins.size();
pins.add(pin);
- Chan.getDatabaseManager().addPin(pin);
+ databaseManager.runTaskSync(databasePinManager.createPin(pin));
- onPinsChanged();
+ updateState();
EventBus.getDefault().post(new PinAddedMessage(pin));
return true;
}
- public boolean addPin(Post opPost) {
- Pin pin = new Pin();
- pin.loadable = LoadablePool.getInstance().obtain(new Loadable(opPost.board, opPost.no));
- pin.loadable.title = PostHelper.getTitle(opPost, pin.loadable);
- pin.thumbnailUrl = opPost.thumbnailUrl;
- return addPin(pin);
- }
+ public void deletePin(Pin pin) {
+ pins.remove(pin);
- public boolean addPin(Loadable loadable, Post opPost) {
- Pin pin = new Pin();
- pin.loadable = loadable;
- pin.loadable.title = PostHelper.getTitle(opPost, loadable);
- pin.thumbnailUrl = opPost.thumbnailUrl;
- return addPin(pin);
- }
+ destroyPinWatcher(pin);
- public void removePin(Pin pin) {
- pins.remove(pin);
- pin.destroyWatcher();
- Chan.getDatabaseManager().removePin(pin);
+ databaseManager.runTaskSync(databasePinManager.deletePin(pin));
// Update the new orders
- updateDatabase();
+ updatePinsInDatabase();
- onPinsChanged();
+ updateState();
EventBus.getDefault().post(new PinRemovedMessage(pin));
}
- /**
- * Update the pin in the database
- *
- * @param pin
- */
public void updatePin(Pin pin) {
- Chan.getDatabaseManager().updatePin(pin);
+ databaseManager.runTaskSync(databasePinManager.updatePin(pin));
- onPinsChanged();
+ updateState();
EventBus.getDefault().post(new PinChangedMessage(pin));
}
- /**
- * Updates all the pins to the database.
- */
- public void updateDatabase() {
- Chan.getDatabaseManager().updatePins(pins);
+ public Pin findPinByLoadable(Loadable other) {
+ for (int i = 0; i < pins.size(); i++) {
+ Pin pin = pins.get(i);
+ if (pin.loadable.equals(other)) {
+ return pin;
+ }
+ }
+
+ return null;
+ }
+
+ public Pin findPinById(int id) {
+ for (int i = 0; i < pins.size(); i++) {
+ Pin pin = pins.get(i);
+ if (pin.id == id) {
+ return pin;
+ }
+ }
+
+ return null;
+ }
+
+ public List getAllPins() {
+ return pins;
+ }
+
+ public List getWatchingPins() {
+ if (isWatchingSettingEnabled()) {
+ List l = new ArrayList<>();
+
+ for (int i = 0; i < pins.size(); i++) {
+ Pin p = pins.get(i);
+ if (p.watching)
+ l.add(p);
+ }
+
+ return l;
+ } else {
+ return Collections.emptyList();
+ }
}
public void toggleWatch(Pin pin) {
pin.watching = !pin.watching;
+ updateState();
EventBus.getDefault().post(new PinChangedMessage(pin));
- onPinsChanged();
- invokeLoadNow();
}
- public void pinWatcherUpdated(Pin pin) {
- EventBus.getDefault().post(new PinChangedMessage(pin));
- onPinsChanged();
- }
+ public void onBottomPostViewed(Pin pin) {
+ if (pin.watchNewCount >= 0) {
+ pin.watchLastCount = pin.watchNewCount;
+ }
+
+ if (pin.quoteNewCount >= 0) {
+ pin.quoteLastCount = pin.quoteNewCount;
+ }
+
+ PinWatcher pinWatcher = getPinWatcher(pin);
+ if (pinWatcher != null) {
+ pinWatcher.onViewed();
+ }
- public void onPinsChanged() {
- updateTimerState(false);
- updateNotificationServiceState();
- updatePinWatchers();
+ updatePin(pin);
}
- public void invokeLoadNow() {
- if (pendingTimer != null) {
- pendingTimer.cancel();
- pendingTimer = null;
- Logger.d(TAG, "Canceled timer");
+ // Called when the app changes foreground state
+ public void onEvent(Chan.ForegroundChangedMessage message) {
+ updateState();
+ if (!message.inForeground) {
+ updatePinsInDatabase();
}
+ }
- updateTimerState(true);
+ // Called when the broadcast scheduled by the alarmmanager was received
+ public void onBroadcastReceived() {
+ if (currentInterval != IntervalType.BACKGROUND) {
+ Logger.e(TAG, "Received a broadcast for a watchmanager update, but the current state is not BACKGROUND. Ignoring the broadcast.");
+ } else if (System.currentTimeMillis() - lastBackgroundUpdateTime < BACKGROUND_UPDATE_MIN_DELAY) {
+ Logger.w(TAG, "Background update broadcast ignored because it was requested too soon");
+ } else {
+ lastBackgroundUpdateTime = System.currentTimeMillis();
+ update(true);
+ }
}
- public void pausePins() {
+ // Called from the button on the notification
+ public void pauseAll() {
List watchingPins = getWatchingPins();
- for (Pin pin : watchingPins) {
+ for (int i = 0; i < watchingPins.size(); i++) {
+ Pin pin = watchingPins.get(i);
pin.watching = false;
}
- onPinsChanged();
- updateDatabase();
+ updateState();
+ updatePinsInDatabase();
- for (Pin pin : getPins()) {
+ List allPins = getAllPins();
+ for (int i = 0; i < allPins.size(); i++) {
+ Pin pin = allPins.get(i);
EventBus.getDefault().post(new PinChangedMessage(pin));
}
}
- public void onEvent(Chan.ForegroundChangedMessage message) {
- updateNotificationServiceState();
- updateTimerState(true);
- if (!message.inForeground) {
- updateDatabase();
- }
- }
-
+ // Called when the user changes the watch enabled preference
public void onWatchEnabledChanged(boolean watchEnabled) {
- updateNotificationServiceState(watchEnabled, getWatchBackgroundEnabled());
- updateTimerState(watchEnabled, getWatchBackgroundEnabled(), false);
- updatePinWatchers(watchEnabled);
- for (Pin pin : getPins()) {
+ updateState(watchEnabled, isBackgroundWatchingSettingEnabled());
+ List pins = getAllPins();
+ for (int i = 0; i < pins.size(); i++) {
+ Pin pin = pins.get(i);
EventBus.getDefault().post(new PinChangedMessage(pin));
}
}
+ // Called when the user changes the watch background enabled preference
public void onBackgroundWatchingChanged(boolean backgroundEnabled) {
- updateNotificationServiceState(getTimerEnabled(), backgroundEnabled);
- updateTimerState(getTimerEnabled(), backgroundEnabled, false);
- for (Pin pin : getPins()) {
+ updateState(isTimerEnabled(), backgroundEnabled);
+ List pins = getAllPins();
+ for (int i = 0; i < pins.size(); i++) {
+ Pin pin = pins.get(i);
EventBus.getDefault().post(new PinChangedMessage(pin));
}
}
- private boolean getTimerEnabled() {
- // getWatchingPins returns an empty list when ChanPreferences.getWatchEnabled() is false
- return getWatchingPins().size() > 0;
+ public PinWatcher getPinWatcher(Pin pin) {
+ return pinWatchers.get(pin);
}
- public boolean getWatchBackgroundEnabled() {
- return ChanSettings.watchBackground.get();
+ private boolean createPinWatcher(Pin pin) {
+ if (!pinWatchers.containsKey(pin)) {
+ pinWatchers.put(pin, new PinWatcher(pin));
+ return true;
+ } else {
+ return false;
+ }
}
- private void updatePinWatchers() {
- updatePinWatchers(ChanSettings.watchEnabled.get());
+ private boolean destroyPinWatcher(Pin pin) {
+ PinWatcher pinWatcher = pinWatchers.remove(pin);
+ if (pinWatcher != null) {
+ pinWatcher.destroy();
+ }
+ return pinWatcher != null;
}
- private void updatePinWatchers(boolean watchEnabled) {
- for (Pin pin : pins) {
- if (watchEnabled) {
- pin.createWatcher();
- } else {
- pin.destroyWatcher();
- }
- }
+ private void updatePinsInDatabase() {
+ databaseManager.runTask(databasePinManager.updatePins(pins));
}
- private void updateNotificationServiceState() {
- updateNotificationServiceState(getTimerEnabled(), getWatchBackgroundEnabled());
+ private Boolean isWatchingSettingEnabled() {
+ return ChanSettings.watchEnabled.get();
}
- private void updateNotificationServiceState(boolean watchEnabled, boolean backgroundEnabled) {
- if (watchEnabled && backgroundEnabled) {
- // Also calls onStartCommand, which updates the notification
- context.startService(new Intent(context, WatchNotifier.class));
- } else {
- context.stopService(new Intent(context, WatchNotifier.class));
- }
+ private boolean isBackgroundWatchingSettingEnabled() {
+ return ChanSettings.watchBackground.get();
+ }
+
+ private boolean isInForeground() {
+ return Chan.getInstance().getApplicationInForeground();
+ }
+
+ private boolean isTimerEnabled() {
+ return !getWatchingPins().isEmpty();
+ }
+
+ private int getBackgroundIntervalSetting() {
+ return ChanSettings.watchBackgroundInterval.get();
}
- private void updateTimerState(boolean invokeLoadNow) {
- updateTimerState(getTimerEnabled(), getWatchBackgroundEnabled(), invokeLoadNow);
+ private void updateState() {
+ updateState(isTimerEnabled(), isBackgroundWatchingSettingEnabled());
}
- private void updateTimerState(boolean watchEnabled, boolean backgroundEnabled, boolean invokeLoadNow) {
- Logger.d(TAG, "updateTimerState watchEnabled=" + watchEnabled + " backgroundEnabled=" + backgroundEnabled + " invokeLoadNow=" + invokeLoadNow + " foreground=" + Chan.getInstance().getApplicationInForeground());
- if (watchEnabled) {
- if (Chan.getInstance().getApplicationInForeground()) {
- setTimer(invokeLoadNow ? 1 : FOREGROUND_TIME);
+ // Update the interval type, according to the current settings,
+ // creates and destroys the PinWatchers where needed and
+ // updates the notification.
+ private void updateState(boolean watchEnabled, boolean backgroundEnabled) {
+ Logger.d(TAG, "updateState watchEnabled=" + watchEnabled + " backgroundEnabled=" + backgroundEnabled + " foreground=" + isInForeground());
+
+ IntervalType intervalType;
+ if (!watchEnabled) {
+ intervalType = IntervalType.NONE;
+ } else {
+ if (isInForeground()) {
+ intervalType = IntervalType.FOREGROUND;
} else {
if (backgroundEnabled) {
- setTimer(Integer.parseInt(ChanSettings.watchBackgroundTimeout.get()));
+ intervalType = IntervalType.BACKGROUND;
} else {
- if (pendingTimer != null) {
- pendingTimer.cancel();
- pendingTimer = null;
- Logger.d(TAG, "Canceled timer");
- }
+ intervalType = IntervalType.NONE;
}
}
- } else {
- if (pendingTimer != null) {
- pendingTimer.cancel();
- pendingTimer = null;
- Logger.d(TAG, "Canceled timer");
+ }
+
+ // Changing interval type, like when watching is disabled or the app goes to the background
+ if (currentInterval != intervalType) {
+ // Handle the preview state
+ switch (currentInterval) {
+ case FOREGROUND:
+ // Stop receiving handler messages
+ handler.removeMessages(MESSAGE_UPDATE);
+ break;
+ case BACKGROUND:
+ // Stop the scheduled broadcast
+ scheduleAlarmManager(false);
+ break;
+ case NONE:
+ // Nothing to do when no timer was set.
+ break;
+ }
+
+ Logger.d(TAG, "Setting interval type from " + currentInterval.name() + " to " + intervalType.name());
+ currentInterval = intervalType;
+
+ switch (currentInterval) {
+ case FOREGROUND:
+ // Schedule a delayed handler that will call update(false)
+ handler.sendMessageDelayed(handler.obtainMessage(MESSAGE_UPDATE), FOREGROUND_INTERVAL);
+ break;
+ case BACKGROUND:
+ // Schedule an intervaled broadcast receiver
+ scheduleAlarmManager(true);
+ break;
+ case NONE:
+ // Nothing to do when no timer is set.
+ break;
}
}
+
+ // Update pin watchers
+ for (int i = 0; i < pins.size(); i++) {
+ Pin pin = pins.get(i);
+ if (watchEnabled && pin.watching) {
+ createPinWatcher(pin);
+ } else {
+ destroyPinWatcher(pin);
+ }
+ }
+
+ // Update notification state
+ if (watchEnabled && backgroundEnabled) {
+ // Also calls onStartCommand, which updates the notification with new info
+ getAppContext().startService(new Intent(getAppContext(), WatchNotifier.class));
+ } else {
+ getAppContext().stopService(new Intent(getAppContext(), WatchNotifier.class));
+ }
}
- private void setTimer(int time) {
- if (pendingTimer != null && pendingTimer.time == time) {
- return;
+ // Schedule a broadcast that calls WatchUpdateReceiver.onReceive() if enabled is true,
+ // and unschedules it when false
+ private void scheduleAlarmManager(boolean enable) {
+ Intent intent = new Intent(WATCHER_UPDATE_ACTION);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(getAppContext(), REQUEST_CODE_WATCH_UPDATE, intent, 0);
+ if (enable) {
+ int interval = getBackgroundIntervalSetting();
+ Logger.d(TAG, "Scheduled for an inexact repeating broadcast receiver with an interval of " + (interval / 1000 / 60) + " minutes");
+ alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, interval, interval, pendingIntent);
+ } else {
+ Logger.d(TAG, "Unscheduled the repeating broadcast receiver");
+ alarmManager.cancel(pendingIntent);
}
+ }
+
+ // Update the watching pins
+ private void update(boolean fromBackground) {
+ Logger.d(TAG, "update() fromBackground = " + fromBackground);
- if (pendingTimer != null) {
- pendingTimer.cancel();
- pendingTimer = null;
- Logger.d(TAG, "Canceled timer");
+ switch (currentInterval) {
+ case FOREGROUND:
+ // reschedule handler message
+ handler.sendMessageDelayed(handler.obtainMessage(MESSAGE_UPDATE), FOREGROUND_INTERVAL);
+ break;
+ case BACKGROUND:
+ // AlarmManager is scheduled as an interval
+ break;
}
- ScheduledFuture scheduledFuture = executor.schedule(new Runnable() {
- @Override
- public void run() {
- AndroidUtils.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- timerFired();
- }
- });
+ // A set of watchers that all have to complete being updated
+ // before the wakelock is released again
+ waitingForPinWatchersForBackgroundUpdate = null;
+ if (fromBackground) {
+ waitingForPinWatchersForBackgroundUpdate = new HashSet<>();
+ }
+
+ List watchingPins = getWatchingPins();
+ for (int i = 0; i < watchingPins.size(); i++) {
+ Pin pin = watchingPins.get(i);
+ PinWatcher pinWatcher = getPinWatcher(pin);
+ if (pinWatcher != null && pinWatcher.update(fromBackground)) {
+ EventBus.getDefault().post(new PinChangedMessage(pin));
+
+ if (fromBackground) {
+ waitingForPinWatchersForBackgroundUpdate.add(pinWatcher);
+ }
}
- }, time, TimeUnit.SECONDS);
- pendingTimer = new PendingTimer(scheduledFuture, time);
- Logger.d(TAG, "Timer firing in " + time + " seconds");
+ }
+
+ if (fromBackground && !waitingForPinWatchersForBackgroundUpdate.isEmpty()) {
+ Logger.i(TAG, "Acquiring wakelock for pin watcher updates");
+ manageLock(true);
+ }
}
- private void timerFired() {
- Logger.d(TAG, "Timer fired");
- pendingTimer = null;
+ private void pinWatcherUpdated(PinWatcher pinWatcher) {
+ updateState();
+ EventBus.getDefault().post(new PinChangedMessage(pinWatcher.pin));
- for (Pin pin : getWatchingPins()) {
- if (pin.update()) {
- EventBus.getDefault().post(new PinChangedMessage(pin));
+ if (waitingForPinWatchersForBackgroundUpdate != null) {
+ waitingForPinWatchersForBackgroundUpdate.remove(pinWatcher);
+
+ if (waitingForPinWatchersForBackgroundUpdate.isEmpty()) {
+ Logger.i(TAG, "All watchers updated, removing wakelock");
+ waitingForPinWatchersForBackgroundUpdate = null;
+ manageLock(false);
}
}
+ }
- updateTimerState(false);
+ private void manageLock(boolean lock) {
+ if (lock) {
+ if (wakeLock != null) {
+ Logger.e(TAG, "Wakelock not null while trying to acquire one");
+ if (wakeLock.isHeld()) {
+ wakeLock.release();
+ }
+ wakeLock = null;
+ }
+ wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
+ wakeLock.setReferenceCounted(false);
+ wakeLock.acquire(WAKELOCK_MAX_TIME);
+ } else {
+ if (wakeLock == null) {
+ Logger.e(TAG, "Wakelock null while trying to release it");
+ } else {
+ if (wakeLock.isHeld()) {
+ wakeLock.release();
+ }
+ wakeLock = null;
+ }
+ }
}
public static class PinAddedMessage {
@@ -381,17 +567,172 @@ public class WatchManager {
}
}
- private static class PendingTimer {
- public ScheduledFuture scheduledFuture;
- public int time;
+ public class PinWatcher implements ChanLoader.ChanLoaderCallback {
+ private static final String TAG = "PinWatcher";
+
+ private final Pin pin;
+ private ChanLoader chanLoader;
+
+ private final List posts = new ArrayList<>();
+ private final List quotes = new ArrayList<>();
+ private boolean wereNewQuotes = false;
+ private boolean wereNewPosts = false;
+
+ public PinWatcher(Pin pin) {
+ this.pin = pin;
+
+ Logger.i(TAG, "PinWatcher: created for " + pin);
+ chanLoader = LoaderPool.getInstance().obtain(pin.loadable, this);
+ }
- public PendingTimer(ScheduledFuture scheduledFuture, int time) {
- this.scheduledFuture = scheduledFuture;
- this.time = time;
+ public List getUnviewedPosts() {
+ if (posts.size() == 0) {
+ return posts;
+ } else {
+ return posts.subList(Math.max(0, posts.size() - pin.getNewPostCount()), posts.size());
+ }
}
- public void cancel() {
- scheduledFuture.cancel(false);
+ public List getUnviewedQuotes() {
+ return quotes.subList(Math.max(0, quotes.size() - pin.getNewQuoteCount()), quotes.size());
+ }
+
+ public boolean getWereNewQuotes() {
+ if (wereNewQuotes) {
+ wereNewQuotes = false;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean getWereNewPosts() {
+ if (wereNewPosts) {
+ wereNewPosts = false;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void destroy() {
+ if (chanLoader != null) {
+ Logger.i(TAG, "PinWatcher: destroyed for " + pin);
+ LoaderPool.getInstance().release(chanLoader, this);
+ chanLoader = null;
+ }
+ }
+
+ private void onViewed() {
+ wereNewPosts = false;
+ wereNewQuotes = false;
+ }
+
+ private boolean update(boolean fromBackground) {
+ if (!pin.isError && pin.watching) {
+ if (fromBackground) {
+ // Always load regardless of timer, since the time left is not accurate for 15min+ intervals
+ chanLoader.requestMoreData();
+ return true;
+ } else {
+ // true if a load was started
+ return chanLoader.loadMoreIfTime();
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /*private long getTimeUntilNextLoad() {
+ return chanLoader.getTimeUntilLoadMore();
+ }*/
+
+ /*public boolean isLoading() {
+ return chanLoader.isLoading();
+ }*/
+
+ @Override
+ public void onChanLoaderError(VolleyError error) {
+ pin.isError = true;
+
+ pin.watching = false;
+
+ pinWatcherUpdated(this);
+ }
+
+ @Override
+ public void onChanLoaderData(ChanThread thread) {
+ pin.isError = false;
+
+ if (pin.thumbnailUrl == null && thread.op != null && thread.op.hasImage) {
+ pin.thumbnailUrl = thread.op.thumbnailUrl;
+ }
+
+ // Populate posts list
+ posts.clear();
+ posts.addAll(thread.posts);
+
+ // Populate quotes list
+ quotes.clear();
+
+ // Get list of saved replies from this thread
+ List savedReplies = new ArrayList<>();
+ for (Post item : thread.posts) {
+ // saved.title = pin.loadable.title;
+
+ if (item.isSavedReply) {
+ savedReplies.add(item);
+ }
+ }
+
+ // Now get a list of posts that have a quote to a saved reply
+ for (Post post : thread.posts) {
+ for (Post saved : savedReplies) {
+ if (post.repliesTo.contains(saved.no)) {
+ quotes.add(post);
+ }
+ }
+ }
+
+ boolean isFirstLoad = pin.watchNewCount < 0 || pin.quoteNewCount < 0;
+
+ // If it was more than before processing
+ int lastWatchNewCount = pin.watchNewCount;
+ int lastQuoteNewCount = pin.quoteNewCount;
+
+ if (isFirstLoad) {
+ pin.watchLastCount = posts.size();
+ pin.quoteLastCount = quotes.size();
+ }
+
+ pin.watchNewCount = posts.size();
+ pin.quoteNewCount = quotes.size();
+
+ if (!isFirstLoad) {
+ // There were new posts after processing
+ if (pin.watchNewCount > lastWatchNewCount) {
+ wereNewPosts = true;
+ }
+
+ // There were new quotes after processing
+ if (pin.quoteNewCount > lastQuoteNewCount) {
+ wereNewQuotes = true;
+ }
+ }
+
+ if (Logger.debugEnabled()) {
+ Logger.d(TAG, String.format(Locale.ENGLISH,
+ "postlast=%d postnew=%d werenewposts=%b quotelast=%d quotenew=%d werenewquotes=%b nextload=%ds",
+ pin.watchLastCount, pin.watchNewCount, wereNewPosts, pin.quoteLastCount,
+ pin.quoteNewCount, wereNewQuotes, chanLoader.getTimeUntilLoadMore() / 1000));
+ }
+
+ if (thread.archived || thread.closed) {
+ pin.archived = true;
+ pin.watching = false;
+ }
+
+ pinWatcherUpdated(this);
}
}
}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Pin.java b/Clover/app/src/main/java/org/floens/chan/core/model/Pin.java
index 80d136d1..4e4b985f 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/model/Pin.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/model/Pin.java
@@ -20,8 +20,6 @@ package org.floens.chan.core.model;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
-import org.floens.chan.core.watch.PinWatcher;
-
@DatabaseTable
public class Pin {
@DatabaseField(generatedId = true)
@@ -57,8 +55,6 @@ public class Pin {
@DatabaseField
public boolean archived = false;
- private PinWatcher pinWatcher;
-
public int getNewPostCount() {
if (watchLastCount < 0 || watchNewCount < 0) {
return 0;
@@ -74,31 +70,4 @@ public class Pin {
return Math.max(0, quoteNewCount - quoteLastCount);
}
}
-
- public PinWatcher getPinWatcher() {
- return pinWatcher;
- }
-
- public void onBottomPostViewed() {
- if (pinWatcher != null) {
- pinWatcher.onViewed();
- }
- }
-
- public boolean update() {
- return pinWatcher != null && watching && pinWatcher.update();
- }
-
- public void createWatcher() {
- if (pinWatcher == null) {
- pinWatcher = new PinWatcher(this);
- }
- }
-
- public void destroyWatcher() {
- if (pinWatcher != null) {
- pinWatcher.destroy();
- pinWatcher = null;
- }
- }
}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
index 87945bca..209b4084 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
@@ -211,7 +211,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback,
if (ChanSettings.postPinThread.get() && loadable.isThreadMode()) {
ChanThread thread = callback.getThread();
if (thread != null) {
- watchManager.addPin(loadable, thread.op);
+ watchManager.createPin(loadable, thread.op);
}
}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
index cd6e70b0..ead7ccb7 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
@@ -162,10 +162,10 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
if (pin == null) {
if (chanLoader.getThread() != null) {
Post op = chanLoader.getThread().op;
- watchManager.addPin(loadable, op);
+ watchManager.createPin(loadable, op);
}
} else {
- watchManager.removePin(pin);
+ watchManager.deletePin(pin);
}
return isPinned();
}
@@ -275,8 +275,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
Pin pin = watchManager.findPinByLoadable(loadable);
if (pin != null) {
- pin.onBottomPostViewed();
- watchManager.updatePin(pin);
+ watchManager.onBottomPostViewed(pin);
}
threadPresenterCallback.showNewPostsNotification(false, -1);
@@ -449,7 +448,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
databaseManager.saveReply(new SavedReply(post.board, post.no, "foo"));
break;
case POST_OPTION_PIN:
- watchManager.addPin(post);
+ Loadable pinLoadable = LoadablePool.getInstance().obtain(new Loadable(post.board, post.no));
+ watchManager.createPin(pinLoadable, post);
break;
case POST_OPTION_OPEN_BROWSER:
AndroidUtils.openLink(
diff --git a/Clover/app/src/main/java/org/floens/chan/core/receiver/WatchUpdateReceiver.java b/Clover/app/src/main/java/org/floens/chan/core/receiver/WatchUpdateReceiver.java
new file mode 100644
index 00000000..2bd775d7
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/core/receiver/WatchUpdateReceiver.java
@@ -0,0 +1,41 @@
+/*
+ * Clover - 4chan browser https://github.com/Floens/Clover/
+ * Copyright (C) 2014 Floens
+ *
+ * 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 .
+ */
+package org.floens.chan.core.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import org.floens.chan.Chan;
+import org.floens.chan.core.manager.WatchManager;
+import org.floens.chan.utils.Logger;
+
+public class WatchUpdateReceiver extends BroadcastReceiver {
+ private static final String TAG = "WatchUpdateReceiver";
+
+ private final WatchManager watchManager;
+
+ public WatchUpdateReceiver() {
+ watchManager = Chan.getWatchManager();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ watchManager.onBroadcastReceived();
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
index acabc8c0..71ff077f 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
@@ -24,6 +24,7 @@ import android.text.TextUtils;
import org.floens.chan.Chan;
import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls;
+import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.ui.adapter.PostsFilter;
import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.utils.AndroidUtils;
@@ -100,9 +101,10 @@ public class ChanSettings {
public static final BooleanSetting watchEnabled;
public static final BooleanSetting watchCountdown;
public static final BooleanSetting watchBackground;
- public static final StringSetting watchBackgroundTimeout;
+ public static final IntegerSetting watchBackgroundInterval;
public static final StringSetting watchNotifyMode;
public static final StringSetting watchSound;
+ public static final BooleanSetting watchPeek;
public static final StringSetting watchLed;
public static final StringSetting passToken;
@@ -184,9 +186,10 @@ public class ChanSettings {
Chan.getWatchManager().onBackgroundWatchingChanged(value);
}
});
- watchBackgroundTimeout = new StringSetting(p, "preference_watch_background_timeout", "60");
+ watchBackgroundInterval = new IntegerSetting(p, "preference_watch_background_interval", WatchManager.DEFAULT_BACKGROUND_INTERVAL);
watchNotifyMode = new StringSetting(p, "preference_watch_notify_mode", "all");
watchSound = new StringSetting(p, "preference_watch_sound", "quotes");
+ watchPeek = new BooleanSetting(p, "preference_watch_peek", true);
watchLed = new StringSetting(p, "preference_watch_led", "ffffffff");
passToken = new StringSetting(p, "preference_pass_token", "");
@@ -227,6 +230,7 @@ public class ChanSettings {
// preference_board_editor_filler default false
// preference_pass_enabled default false
// preference_autoplay false
+ // preference_watch_background_timeout "60" the old timeout background setting in minutes
}
public static boolean passLoggedIn() {
diff --git a/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java b/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java
deleted file mode 100644
index d0a32074..00000000
--- a/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Clover - 4chan browser https://github.com/Floens/Clover/
- * Copyright (C) 2014 Floens
- *
- * 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 .
- */
-package org.floens.chan.core.watch;
-
-import com.android.volley.VolleyError;
-
-import org.floens.chan.Chan;
-import org.floens.chan.chan.ChanLoader;
-import org.floens.chan.core.model.ChanThread;
-import org.floens.chan.core.model.Pin;
-import org.floens.chan.core.model.Post;
-import org.floens.chan.core.pool.LoaderPool;
-import org.floens.chan.utils.Logger;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PinWatcher implements ChanLoader.ChanLoaderCallback {
- private static final String TAG = "PinWatcher";
-
- private final Pin pin;
- private ChanLoader chanLoader;
-
- private final List posts = new ArrayList<>();
- private final List quotes = new ArrayList<>();
- private boolean wereNewQuotes = false;
- private boolean wereNewPosts = false;
-
- public PinWatcher(Pin pin) {
- this.pin = pin;
-
- chanLoader = LoaderPool.getInstance().obtain(pin.loadable, this);
- }
-
- public void destroy() {
- if (chanLoader != null) {
- LoaderPool.getInstance().release(chanLoader, this);
- chanLoader = null;
- }
- }
-
- public boolean update() {
- if (!pin.isError) {
- chanLoader.loadMoreIfTime();
- return true;
- } else {
- return false;
- }
- }
-
- public void onViewed() {
- if (pin.watchNewCount >= 0) {
- pin.watchLastCount = pin.watchNewCount;
- }
- wereNewPosts = false;
-
- if (pin.quoteNewCount >= 0) {
- pin.quoteLastCount = pin.quoteNewCount;
- }
- wereNewQuotes = false;
- }
-
- public List getUnviewedPosts() {
- if (posts.size() == 0) {
- return posts;
- } else {
- return posts.subList(Math.max(0, posts.size() - pin.getNewPostCount()), posts.size());
- }
- }
-
- public List getUnviewedQuotes() {
- return quotes.subList(Math.max(0, quotes.size() - pin.getNewQuoteCount()), quotes.size());
- }
-
- public boolean getWereNewQuotes() {
- if (wereNewQuotes) {
- wereNewQuotes = false;
- return true;
- } else {
- return false;
- }
- }
-
- public boolean getWereNewPosts() {
- if (wereNewPosts) {
- wereNewPosts = false;
- return true;
- } else {
- return false;
- }
- }
-
- public long getTimeUntilNextLoad() {
- return chanLoader.getTimeUntilLoadMore();
- }
-
- public boolean isLoading() {
- return chanLoader.isLoading();
- }
-
- @Override
- public void onChanLoaderError(VolleyError error) {
- Logger.e(TAG, "PinWatcher onError");
- pin.isError = true;
-
- pin.watching = false;
- Chan.getWatchManager().pinWatcherUpdated(pin);
- }
-
- @Override
- public void onChanLoaderData(ChanThread thread) {
- pin.isError = false;
-
- if (pin.thumbnailUrl == null && thread.op != null && thread.op.hasImage) {
- pin.thumbnailUrl = thread.op.thumbnailUrl;
- }
-
- // Populate posts list
- posts.clear();
- posts.addAll(thread.posts);
-
- // Populate quotes list
- quotes.clear();
-
- // Get list of saved replies from this thread
- List savedReplies = new ArrayList<>();
- for (Post item : thread.posts) {
-// saved.title = pin.loadable.title;
-
- if (item.isSavedReply) {
- savedReplies.add(item);
- }
- }
-
- // Now get a list of posts that have a quote to a saved reply
- for (Post post : thread.posts) {
- for (Post saved : savedReplies) {
- if (post.repliesTo.contains(saved.no)) {
- quotes.add(post);
- }
- }
- }
-
- boolean isFirstLoad = pin.watchNewCount < 0 || pin.quoteNewCount < 0;
-
- // If it was more than before processing
- int lastWatchNewCount = pin.watchNewCount;
- int lastQuoteNewCount = pin.quoteNewCount;
-
- if (isFirstLoad) {
- pin.watchLastCount = posts.size();
- pin.quoteLastCount = quotes.size();
- }
-
- pin.watchNewCount = posts.size();
- pin.quoteNewCount = quotes.size();
-
- if (!isFirstLoad) {
- // There were new posts after processing
- if (pin.watchNewCount > lastWatchNewCount) {
- wereNewPosts = true;
- }
-
- // There were new quotes after processing
- if (pin.quoteNewCount > lastQuoteNewCount) {
- wereNewQuotes = true;
- }
- }
-
- if (Logger.debugEnabled()) {
- Logger.d(TAG, String.format("postlast=%d postnew=%d werenewposts=%b quotelast=%d quotenew=%d werenewquotes=%b",
- pin.watchLastCount, pin.watchNewCount, wereNewPosts, pin.quoteLastCount, pin.quoteNewCount, wereNewQuotes));
- }
-
- if (thread.archived || thread.closed) {
- pin.archived = true;
- pin.watching = false;
- }
-
- Chan.getWatchManager().pinWatcherUpdated(pin);
- }
-}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java
index b7238c14..4939b8b9 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java
@@ -90,7 +90,7 @@ public class DrawerController extends Controller implements PinAdapter.Callback,
pinAdapter = new PinAdapter(this);
recyclerView.setAdapter(pinAdapter);
- pinAdapter.onPinsChanged(watchManager.getPins());
+ pinAdapter.onPinsChanged(watchManager.getAllPins());
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(pinAdapter.getItemTouchHelperCallback());
itemTouchHelper.attachToRecyclerView(recyclerView);
@@ -157,14 +157,14 @@ public class DrawerController extends Controller implements PinAdapter.Callback,
@Override
public void onPinRemoved(final Pin pin) {
- watchManager.removePin(pin);
+ watchManager.deletePin(pin);
Snackbar snackbar = Snackbar.make(drawerLayout, context.getString(R.string.drawer_pin_removed, pin.loadable.title), Snackbar.LENGTH_LONG);
fixSnackbarText(context, snackbar);
snackbar.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
- watchManager.addPin(pin);
+ watchManager.createPin(pin);
}
});
snackbar.show();
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
index 14de283b..6340d57b 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
@@ -56,8 +56,8 @@ import static org.floens.chan.utils.AndroidUtils.getString;
public class MainSettingsController extends SettingsController implements ToolbarMenuItem.ToolbarMenuItemCallback, WatchSettingsController.WatchSettingControllerListener, PassSettingsController.PassSettingControllerListener {
private static final int ADVANCED_SETTINGS = 1;
- private ListSettingView imageAutoLoadView;
- private ListSettingView videoAutoLoadView;
+ private ListSettingView imageAutoLoadView;
+ private ListSettingView videoAutoLoadView;
private LinkSettingView watchLink;
private LinkSettingView passLink;
@@ -182,7 +182,7 @@ public class MainSettingsController extends SettingsController implements Toolba
fontSizes.add(new ListSettingView.Item(name, String.valueOf(size)));
}
- fontView = appearance.add(new ListSettingView(this, ChanSettings.fontSize, R.string.setting_font_size, fontSizes.toArray(new ListSettingView.Item[fontSizes.size()])));
+ fontView = appearance.add(new ListSettingView<>(this, ChanSettings.fontSize, R.string.setting_font_size, fontSizes.toArray(new ListSettingView.Item[fontSizes.size()])));
fontCondensed = appearance.add(new BooleanSettingView(this, ChanSettings.fontCondensed, R.string.setting_font_condensed, R.string.setting_font_condensed_description));
groups.add(appearance);
@@ -215,13 +215,13 @@ public class MainSettingsController extends SettingsController implements Toolba
break;
}
- imageAutoLoadTypes.add(new ListSettingView.Item(getString(name), mode.name));
- videoAutoLoadTypes.add(new ListSettingView.Item(getString(name), mode.name));
+ imageAutoLoadTypes.add(new ListSettingView.Item<>(getString(name), mode.name));
+ videoAutoLoadTypes.add(new ListSettingView.Item<>(getString(name), mode.name));
}
- imageAutoLoadView = new ListSettingView(this, ChanSettings.imageAutoLoadNetwork, R.string.setting_image_auto_load, imageAutoLoadTypes);
+ imageAutoLoadView = new ListSettingView<>(this, ChanSettings.imageAutoLoadNetwork, R.string.setting_image_auto_load, imageAutoLoadTypes);
browsing.add(imageAutoLoadView);
- videoAutoLoadView = new ListSettingView(this, ChanSettings.videoAutoLoadNetwork, R.string.setting_video_auto_load, videoAutoLoadTypes);
+ videoAutoLoadView = new ListSettingView<>(this, ChanSettings.videoAutoLoadNetwork, R.string.setting_video_auto_load, videoAutoLoadTypes);
browsing.add(videoAutoLoadView);
updateVideoLoadModes();
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java
index 6a5dc899..dc1475b6 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java
@@ -39,6 +39,7 @@ public class WatchSettingsController extends SettingsController implements Compo
private SettingView backgroundTimeout;
private SettingView notifyMode;
private SettingView soundMode;
+ private SettingView peekMode;
private SettingView ledMode;
public WatchSettingsController(Context context) {
@@ -72,6 +73,7 @@ public class WatchSettingsController extends SettingsController implements Compo
setSettingViewVisibility(backgroundTimeout, false, false);
setSettingViewVisibility(notifyMode, false, false);
setSettingViewVisibility(soundMode, false, false);
+ setSettingViewVisibility(peekMode, false, false);
setSettingViewVisibility(ledMode, false, false);
}
}
@@ -94,6 +96,7 @@ public class WatchSettingsController extends SettingsController implements Compo
setSettingViewVisibility(backgroundTimeout, enabled, true);
setSettingViewVisibility(notifyMode, enabled, true);
setSettingViewVisibility(soundMode, enabled, true);
+ setSettingViewVisibility(peekMode, enabled, true);
setSettingViewVisibility(ledMode, enabled, true);
}
}
@@ -104,26 +107,36 @@ public class WatchSettingsController extends SettingsController implements Compo
// settings.add(new BooleanSettingView(this, ChanSettings.watchCountdown, string(R.string.setting_watch_countdown), string(R.string.setting_watch_countdown_description)));
enableBackground = settings.add(new BooleanSettingView(this, ChanSettings.watchBackground, R.string.setting_watch_enable_background, R.string.setting_watch_enable_background_description));
- int[] timeouts = new int[]{1, 2, 3, 5, 10, 30, 60};
+ int[] timeouts = new int[]{
+ 10 * 60 * 1000,
+ 15 * 60 * 1000,
+ 30 * 60 * 1000,
+ 45 * 60 * 1000,
+ 60 * 60 * 1000,
+ 2 * 60 * 60 * 1000
+ };
ListSettingView.Item[] timeoutsItems = new ListSettingView.Item[timeouts.length];
for (int i = 0; i < timeouts.length; i++) {
- String name = context.getResources().getQuantityString(R.plurals.minutes, timeouts[i], timeouts[i]);
- timeoutsItems[i] = new ListSettingView.Item(name, String.valueOf(timeouts[i] * 60));
+ int value = timeouts[i] / 1000 / 60;
+ String name = context.getResources().getQuantityString(R.plurals.minutes, value, value);
+ timeoutsItems[i] = new ListSettingView.Item<>(name, timeouts[i]);
}
- backgroundTimeout = settings.add(new ListSettingView(this, ChanSettings.watchBackgroundTimeout, R.string.setting_watch_background_timeout, timeoutsItems) {
+ backgroundTimeout = settings.add(new ListSettingView(this, ChanSettings.watchBackgroundInterval, R.string.setting_watch_background_timeout, timeoutsItems) {
@Override
public String getBottomDescription() {
return getString(R.string.setting_watch_background_timeout_description) + "\n\n" + items.get(selected).name;
}
});
- notifyMode = settings.add(new ListSettingView(this, ChanSettings.watchNotifyMode, R.string.setting_watch_notify_mode,
+ notifyMode = settings.add(new ListSettingView<>(this, ChanSettings.watchNotifyMode, R.string.setting_watch_notify_mode,
context.getResources().getStringArray(R.array.setting_watch_notify_modes), new String[]{"all", "quotes"}));
- soundMode = settings.add(new ListSettingView(this, ChanSettings.watchSound, R.string.setting_watch_sound,
+ soundMode = settings.add(new ListSettingView<>(this, ChanSettings.watchSound, R.string.setting_watch_sound,
context.getResources().getStringArray(R.array.setting_watch_sounds), new String[]{"all", "quotes"}));
- ledMode = settings.add(new ListSettingView(this, ChanSettings.watchLed, R.string.setting_watch_led,
+ peekMode = settings.add(new BooleanSettingView(this, ChanSettings.watchPeek, R.string.setting_watch_peek, R.string.setting_watch_peek_description));
+
+ ledMode = settings.add(new ListSettingView<>(this, ChanSettings.watchLed, R.string.setting_watch_led,
context.getResources().getStringArray(R.array.setting_watch_leds),
new String[]{"-1", "ffffffff", "ffff0000", "ffffff00", "ff00ff00", "ff00ffff", "ff0000ff", "ffff00ff"}));
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java b/Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java
index 5199bb59..571dca96 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java
@@ -25,6 +25,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
import org.floens.chan.Chan;
import org.floens.chan.R;
@@ -32,19 +33,21 @@ import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.settings.ChanSettings;
-import org.floens.chan.core.watch.PinWatcher;
import org.floens.chan.ui.activity.BoardActivity;
-import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.regex.Pattern;
public class WatchNotifier extends Service {
private static final String TAG = "WatchNotifier";
private static final int NOTIFICATION_ID = 1;
- private static final PostAgeComparer POST_AGE_COMPARER = new PostAgeComparer();
+ private static final PostAgeComparator POST_AGE_COMPARATOR = new PostAgeComparator();
+ private static final int SUBJECT_LENGTH = 6;
+ private static final String IMAGE_TEXT = "(img) ";
+ private static final Pattern SHORTEN_NO_PATTERN = Pattern.compile(">>\\d+(?=\\d{3})(\\d{3})");
private NotificationManager nm;
private WatchManager wm;
@@ -86,49 +89,54 @@ public class WatchNotifier extends Service {
}
public void pausePins() {
- wm.pausePins();
+ wm.pauseAll();
}
private Notification createNotification() {
boolean notifyQuotesOnly = ChanSettings.watchNotifyMode.get().equals("quotes");
boolean soundQuotesOnly = ChanSettings.watchSound.get().equals("quotes");
- List list = new ArrayList<>();
+ List unviewedPosts = new ArrayList<>();
List listQuoting = new ArrayList<>();
List pins = new ArrayList<>();
List subjectPins = new ArrayList<>();
- boolean ticker = false;
+ boolean light = false;
boolean sound = false;
+ boolean peek = false;
for (Pin pin : wm.getWatchingPins()) {
- PinWatcher watcher = pin.getPinWatcher();
- if (watcher == null || pin.isError)
+ WatchManager.PinWatcher watcher = wm.getPinWatcher(pin);
+ if (watcher == null || pin.isError) {
continue;
+ }
pins.add(pin);
if (notifyQuotesOnly) {
- list.addAll(watcher.getUnviewedQuotes());
+ unviewedPosts.addAll(watcher.getUnviewedQuotes());
listQuoting.addAll(watcher.getUnviewedQuotes());
if (watcher.getWereNewQuotes()) {
- ticker = true;
+ light = true;
sound = true;
+ peek = true;
}
if (pin.getNewQuoteCount() > 0) {
subjectPins.add(pin);
}
} else {
- list.addAll(watcher.getUnviewedPosts());
+ unviewedPosts.addAll(watcher.getUnviewedPosts());
listQuoting.addAll(watcher.getUnviewedQuotes());
if (watcher.getWereNewPosts()) {
- ticker = true;
+ light = true;
if (!soundQuotesOnly) {
sound = true;
+ peek = true;
}
}
if (watcher.getWereNewQuotes()) {
sound = true;
+ peek = true;
}
if (pin.getNewPostCount() > 0) {
subjectPins.add(pin);
@@ -137,63 +145,69 @@ public class WatchNotifier extends Service {
}
if (Chan.getInstance().getApplicationInForeground()) {
- ticker = false;
+ light = false;
sound = false;
}
- return notifyAboutPosts(pins, subjectPins, list, listQuoting, notifyQuotesOnly, ticker, sound);
+ if (!ChanSettings.watchPeek.get()) {
+ peek = false;
+ }
+
+ return notifyAboutPosts(pins, subjectPins, unviewedPosts, listQuoting, notifyQuotesOnly, light, sound, peek);
}
- private Notification notifyAboutPosts(List pins, List subjectPins, List list, List listQuoting,
- boolean notifyQuotesOnly, boolean makeTicker, boolean makeSound) {
+ private Notification notifyAboutPosts(List pins, List subjectPins, List unviewedPosts, List listQuoting,
+ boolean notifyQuotesOnly, boolean light, boolean sound, boolean peek) {
String title = getResources().getQuantityString(R.plurals.watch_title, pins.size(), pins.size());
- if (list.size() == 0) {
+ if (unviewedPosts.isEmpty()) {
// Idle notification
String message = getString(R.string.watch_idle);
- return getNotificationFor(null, title, message, -1, null, false, false, false, null);
+ return get(title, message, null, false, false, false, false, null);
} else {
// New posts notification
String message;
- List notificationList;
+ List postsForExpandedLines;
if (notifyQuotesOnly) {
message = getResources().getQuantityString(R.plurals.watch_new_quotes, listQuoting.size(), listQuoting.size());
- notificationList = listQuoting;
+ postsForExpandedLines = listQuoting;
} else {
- notificationList = list;
+ postsForExpandedLines = unviewedPosts;
if (listQuoting.size() > 0) {
- message = getResources().getQuantityString(R.plurals.watch_new_quoting, list.size(), list.size(), listQuoting.size());
+ message = getResources().getQuantityString(R.plurals.watch_new_quoting, unviewedPosts.size(), unviewedPosts.size(), listQuoting.size());
} else {
- message = getResources().getQuantityString(R.plurals.watch_new, list.size(), list.size());
+ message = getResources().getQuantityString(R.plurals.watch_new, unviewedPosts.size(), unviewedPosts.size());
}
}
- Collections.sort(notificationList, POST_AGE_COMPARER);
- List lines = new ArrayList<>();
- for (Post post : notificationList) {
- CharSequence prefix = AndroidUtils.ellipsize(post.title, 18);
-
- CharSequence comment;
- if (post.comment.length() == 0) {
- comment = "(image)";
+ Collections.sort(postsForExpandedLines, POST_AGE_COMPARATOR);
+ List expandedLines = new ArrayList<>();
+ for (Post postForExpandedLine : postsForExpandedLines) {
+ CharSequence prefix;
+ if (postForExpandedLine.title.length() <= SUBJECT_LENGTH) {
+ prefix = postForExpandedLine.title;
} else {
- comment = post.comment;
+ prefix = postForExpandedLine.title.subSequence(0, SUBJECT_LENGTH);
}
- lines.add(prefix + ": " + comment);
- }
+ String comment = postForExpandedLine.hasImage ? IMAGE_TEXT : "";
+ if (postForExpandedLine.comment.length() > 0) {
+ comment += postForExpandedLine.comment;
+ }
- Pin subject = null;
- if (subjectPins.size() == 1) {
- subject = subjectPins.get(0);
+ // Replace >>132456798 with >789 to shorten the notification
+ comment = SHORTEN_NO_PATTERN.matcher(comment).replaceAll(">$1");
+
+ expandedLines.add(prefix + ": " + comment);
}
- String ticker = null;
- if (makeTicker) {
- ticker = message;
+ Pin targetPin = null;
+ if (subjectPins.size() == 1) {
+ targetPin = subjectPins.get(0);
}
- return getNotificationFor(ticker, title, message, -1, lines, makeTicker, makeSound, true, subject);
+ String smallText = TextUtils.join(", ", expandedLines);
+ return get(message, smallText, expandedLines, light, sound, peek, true, targetPin);
}
}
@@ -201,17 +215,16 @@ public class WatchNotifier extends Service {
* Create a notification with the supplied parameters.
* The style of the big notification is InboxStyle, a list of text.
*
- * @param tickerText The tickertext to show, or null if no tickertext should be shown.
- * @param contentTitle The title of the notification
- * @param contentText The content of the small notification
- * @param contentNumber The contentInfo and number, or -1 if not shown
+ * @param title The title of the notification
+ * @param smallText The content of the small notification
* @param expandedLines A list of lines for the big notification, or null if not shown
- * @param makeSound Should the notification make a sound
+ * @param sound Should the notification make a sound
+ * @param peek Peek the notification into the screen
+ * @param alertIcon Show the alert version of the icon
* @param target The target pin, or null to open the pinned pane on tap
*/
- @SuppressWarnings("deprecation")
- private Notification getNotificationFor(String tickerText, String contentTitle, String contentText, int contentNumber,
- List expandedLines, boolean light, boolean makeSound, boolean alert, Pin target) {
+ private Notification get(String title, String smallText, List expandedLines,
+ boolean light, boolean sound, boolean peek, boolean alertIcon, Pin target) {
Intent intent = new Intent(this, BoardActivity.class);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
@@ -223,7 +236,7 @@ public class WatchNotifier extends Service {
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
- if (makeSound) {
+ if (sound || peek) {
builder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
}
@@ -234,24 +247,21 @@ public class WatchNotifier extends Service {
}
}
-
builder.setContentIntent(pendingIntent);
- if (tickerText != null) {
- tickerText = tickerText.substring(0, Math.min(tickerText.length(), 50));
+ builder.setContentTitle(title);
+ if (smallText != null) {
+ builder.setContentText(smallText);
}
- builder.setTicker(tickerText);
- builder.setContentTitle(contentTitle);
- builder.setContentText(contentText);
-
- if (contentNumber >= 0) {
- builder.setContentInfo(Integer.toString(contentNumber));
- builder.setNumber(contentNumber);
+ if (alertIcon || peek) {
+ builder.setSmallIcon(R.drawable.ic_stat_notify_alert);
+ builder.setPriority(NotificationCompat.PRIORITY_HIGH);
+ } else {
+ builder.setSmallIcon(R.drawable.ic_stat_notify);
+ builder.setPriority(NotificationCompat.PRIORITY_MIN);
}
- builder.setSmallIcon(alert ? R.drawable.ic_stat_notify_alert : R.drawable.ic_stat_notify);
-
Intent pauseWatching = new Intent(this, WatchNotifier.class);
pauseWatching.putExtra("pause_pins", true);
@@ -266,14 +276,14 @@ public class WatchNotifier extends Service {
for (CharSequence line : expandedLines.subList(Math.max(0, expandedLines.size() - 10), expandedLines.size())) {
style.addLine(line);
}
- style.setBigContentTitle(contentTitle);
+ style.setBigContentTitle(title);
builder.setStyle(style);
}
- return builder.getNotification();
+ return builder.build();
}
- private static class PostAgeComparer implements Comparator {
+ private static class PostAgeComparator implements Comparator {
@Override
public int compare(Post lhs, Post rhs) {
if (lhs.time < rhs.time) {
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/settings/ListSettingView.java b/Clover/app/src/main/java/org/floens/chan/ui/settings/ListSettingView.java
index d9325a8d..f0cc0710 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/settings/ListSettingView.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/settings/ListSettingView.java
@@ -31,43 +31,43 @@ import java.util.List;
import static org.floens.chan.utils.AndroidUtils.dp;
-public class ListSettingView extends SettingView implements FloatingMenu.FloatingMenuCallback, View.OnClickListener {
+public class ListSettingView extends SettingView implements FloatingMenu.FloatingMenuCallback, View.OnClickListener {
public final List- items;
public int selected;
- private Setting setting;
+ private Setting setting;
- public ListSettingView(SettingsController settingsController, Setting setting, int name, String[] itemNames, String[] keys) {
+ public ListSettingView(SettingsController settingsController, Setting setting, int name, String[] itemNames, String[] keys) {
this(settingsController, setting, getString(name), itemNames, keys);
}
- public ListSettingView(SettingsController settingsController, Setting setting, String name, String[] itemNames, String[] keys) {
+ public ListSettingView(SettingsController settingsController, Setting setting, String name, String[] itemNames, String[] keys) {
super(settingsController, name);
this.setting = setting;
items = new ArrayList<>(itemNames.length);
for (int i = 0; i < itemNames.length; i++) {
- items.add(i, new Item(itemNames[i], keys[i]));
+ items.add(i, new Item<>(itemNames[i], keys[i]));
}
updateSelection();
}
- public ListSettingView(SettingsController settingsController, Setting setting, int name, Item[] items) {
+ public ListSettingView(SettingsController settingsController, Setting setting, int name, Item[] items) {
this(settingsController, setting, getString(name), items);
}
- public ListSettingView(SettingsController settingsController, Setting setting, int name, List
- items) {
+ public ListSettingView(SettingsController settingsController, Setting setting, int name, List
- items) {
this(settingsController, setting, getString(name), items);
}
- public ListSettingView(SettingsController settingsController, Setting setting, String name, Item[] items) {
+ public ListSettingView(SettingsController settingsController, Setting setting, String name, Item[] items) {
this(settingsController, setting, name, Arrays.asList(items));
}
- public ListSettingView(SettingsController settingsController, Setting setting, String name, List
- items) {
+ public ListSettingView(SettingsController settingsController, Setting setting, String name, List
- items) {
super(settingsController, name);
this.setting = setting;
this.items = items;
@@ -79,7 +79,7 @@ public class ListSettingView extends SettingView implements FloatingMenu.Floatin
return items.get(selected).name;
}
- public Setting getSetting() {
+ public Setting getSetting() {
return setting;
}
@@ -114,9 +114,10 @@ public class ListSettingView extends SettingView implements FloatingMenu.Floatin
menu.show();
}
+ @SuppressWarnings("unchecked")
@Override
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
- String selectedKey = (String) item.getId();
+ T selectedKey = (T) item.getId();
setting.set(selectedKey);
updateSelection();
settingsController.onPreferenceChange(this);
@@ -127,7 +128,7 @@ public class ListSettingView extends SettingView implements FloatingMenu.Floatin
}
public void updateSelection() {
- String selectedKey = setting.get();
+ T selectedKey = setting.get();
for (int i = 0; i < items.size(); i++) {
if (items.get(i).key.equals(selectedKey)) {
selected = i;
@@ -136,18 +137,18 @@ public class ListSettingView extends SettingView implements FloatingMenu.Floatin
}
}
- public static class Item {
+ public static class Item {
public final String name;
- public final String key;
+ public final T key;
public boolean enabled;
- public Item(String name, String key) {
+ public Item(String name, T key) {
this.name = name;
this.key = key;
enabled = true;
}
- public Item(String name, String key, boolean enabled) {
+ public Item(String name, T key, boolean enabled) {
this.name = name;
this.key = key;
this.enabled = enabled;
diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml
index e5aa1956..82598b0d 100644
--- a/Clover/app/src/main/res/values/strings.xml
+++ b/Clover/app/src/main/res/values/strings.xml
@@ -400,18 +400,20 @@ along with this program. If not, see .
Off
Enable in the background
Watch pins when Clover is in the background
- Minimum time between loads in background
- Minimum time between loads, with exponential backoff
+ Background update interval
+ The time between updates in the background
Notify about
- All posts
- Only posts quoting you
- Notification sound and vibrate
+ Notification sound
- All posts
- Only posts quoting you
+ Heads-up notification on mentions
+ Show a heads-up notification on mentions
Notification light
- None