diff --git a/Clover/app/src/main/AndroidManifest.xml b/Clover/app/src/main/AndroidManifest.xml index 59af3ef0..d68c023a 100644 --- a/Clover/app/src/main/AndroidManifest.xml +++ b/Clover/app/src/main/AndroidManifest.xml @@ -15,15 +15,15 @@ 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 . --> - - - - - + + + + + . android:label="@string/app_name"> - - + + - - + + - - + + - - - - - + + + + + - + + android:exported="false" /> + android:exported="false" /> + + + + + + + + diff --git a/Clover/app/src/main/java/org/floens/chan/Chan.java b/Clover/app/src/main/java/org/floens/chan/Chan.java index 85108f6a..6a9dc083 100644 --- a/Clover/app/src/main/java/org/floens/chan/Chan.java +++ b/Clover/app/src/main/java/org/floens/chan/Chan.java @@ -129,7 +129,7 @@ public class Chan extends Application { databaseManager = new DatabaseManager(this); boardManager = new BoardManager(); - watchManager = new WatchManager(this); + watchManager = new WatchManager(); Time.endTiming("Initializing application", startTime); diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java index dd68cf2b..a98b8df3 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java @@ -53,7 +53,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener pendingFuture; @@ -122,24 +122,25 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener lastPostCount) { + lastPostCount = postCount; + currentTimeout = 0; + } else { + currentTimeout = Math.min(currentTimeout + 1, watchTimeouts.length - 1); + } + for (ChanLoaderCallback l : listeners) { l.onChanLoaderData(thread); } @@ -262,15 +271,6 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener lastPostCount) { - lastPostCount = postCount; - currentTimeout = 0; - } else { - currentTimeout++; - currentTimeout = Math.min(currentTimeout, watchTimeouts.length - 1); - } - int watchTimeout = watchTimeouts[currentTimeout]; Logger.d(TAG, "Scheduled reload in " + watchTimeout + "s"); @@ -289,7 +289,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener historyByLoadable = new HashMap<>(); + private final DatabasePinManager databasePinManager; + public DatabaseManager(Context context) { helper = new DatabaseHelper(context); + databasePinManager = new DatabasePinManager(this, helper); + initialize(); + } + + private void initialize() { + loadSavedReplies(); + loadThreadHides(); + loadHistory(); + } + + /** + * Reset all tables in the database. Used for the developer screen. + */ + public void reset() { + helper.reset(); initialize(); } @@ -127,91 +145,8 @@ public class DatabaseManager { return getSavedReply(board, no) != null; } - /** - * Adds a {@link Pin} to the pin table. - * - * @param pin Pin to save - */ - public void addPin(Pin pin) { - try { - helper.loadableDao.create(pin.loadable); - helper.pinDao.create(pin); - } catch (SQLException e) { - Logger.e(TAG, "Error adding pin to db", e); - } - } - - /** - * Deletes a {@link Pin} from the pin table. - * - * @param pin Pin to delete - */ - public void removePin(Pin pin) { - try { - helper.pinDao.delete(pin); - helper.loadableDao.delete(pin.loadable); - } catch (SQLException e) { - Logger.e(TAG, "Error removing pin from db", e); - } - } - - /** - * Updates a {@link Pin} in the pin table. - * - * @param pin Pin to update - */ - public void updatePin(Pin pin) { - try { - helper.pinDao.update(pin); - helper.loadableDao.update(pin.loadable); - } catch (SQLException e) { - Logger.e(TAG, "Error updating pin in db", e); - } - } - - /** - * Updates all {@link Pin}s in the list to the pin table. - * - * @param pins Pins to update - */ - public void updatePins(final List pins) { - try { - callInTransaction(helper.getConnectionSource(), new Callable() { - @Override - public Void call() throws SQLException { - for (Pin pin : pins) { - helper.pinDao.update(pin); - } - - for (Pin pin : pins) { - helper.loadableDao.update(pin.loadable); - } - - return null; - } - }); - } catch (Exception e) { - Logger.e(TAG, "Error updating pins in db", e); - } - } - - /** - * Get a list of {@link Pin}s from the pin table. - * - * @return List of Pins - */ - public List getPinned() { - List list = null; - try { - list = helper.pinDao.queryForAll(); - for (Pin p : list) { - helper.loadableDao.refresh(p.loadable); - } - } catch (SQLException e) { - Logger.e(TAG, "Error getting pins from db", e); - } - - return list; + public DatabasePinManager getDatabasePinManager() { + return databasePinManager; } /** @@ -431,20 +366,6 @@ public class DatabaseManager { return o; } - /** - * Reset all tables in the database. Used for the developer screen. - */ - public void reset() { - helper.reset(); - initialize(); - } - - private void initialize() { - loadSavedReplies(); - loadThreadHides(); - loadHistory(); - } - /** * Threadsafe. */ @@ -541,4 +462,45 @@ public class DatabaseManager { Logger.e(TAG, "Error trimming table " + table, e); } } + + public void runTask(final Callable taskCallable) { + runTask(taskCallable, null); + } + + public void runTask(final Callable taskCallable, final TaskResult taskResult) { + backgroundExecutor.submit(new Runnable() { + @Override + public void run() { + try { + T result = TransactionManager.callInTransaction(helper.getConnectionSource(), taskCallable); + if (taskResult != null) { + completeTask(taskResult, result); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + + public T runTaskSync(final Callable taskCallable) { + try { + return TransactionManager.callInTransaction(helper.getConnectionSource(), taskCallable); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void completeTask(final TaskResult task, final T result) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + task.onComplete(result); + } + }); + } + + public interface TaskResult { + void onComplete(T result); + } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java new file mode 100644 index 00000000..a208c2c6 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java @@ -0,0 +1,102 @@ +/* + * 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.database; + +import org.floens.chan.core.model.Pin; + +import java.util.List; +import java.util.concurrent.Callable; + +public class DatabasePinManager { + private static final String TAG = "DatabasePinManager"; + + private DatabaseManager databaseManager; + private DatabaseHelper helper; + + public DatabasePinManager(DatabaseManager databaseManager, DatabaseHelper helper) { + this.databaseManager = databaseManager; + this.helper = helper; + } + + public Callable createPin(final Pin pin) { + return new Callable() { + @Override + public Pin call() throws Exception { + helper.loadableDao.create(pin.loadable); + helper.pinDao.create(pin); + return pin; + } + }; + } + + public Callable deletePin(final Pin pin) { + return new Callable() { + @Override + public Void call() throws Exception { + helper.pinDao.delete(pin); + helper.loadableDao.delete(pin.loadable); + + return null; + } + }; + } + + public Callable updatePin(final Pin pin) { + return new Callable() { + @Override + public Pin call() throws Exception { + helper.pinDao.update(pin); + helper.loadableDao.update(pin.loadable); + return pin; + } + }; + } + + public Callable> updatePins(final List pins) { + return new Callable>() { + @Override + public List call() throws Exception { + for (int i = 0; i < pins.size(); i++) { + Pin pin = pins.get(i); + helper.pinDao.update(pin); + } + + for (int i = 0; i < pins.size(); i++) { + Pin pin = pins.get(i); + helper.loadableDao.update(pin.loadable); + } + + return null; + } + }; + } + + public Callable> getPins() { + return new Callable>() { + @Override + public List call() throws Exception { + List 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