From 134d745bc595224c2218858bf804462dab47d402 Mon Sep 17 00:00:00 2001 From: Floens Date: Wed, 6 Apr 2016 14:50:20 +0200 Subject: [PATCH] Refactor WatchManager Use structured interval types: a handler for foreground update intervals and the alarmmanager for background update intervals. Fix issue where the backoff would not work on pin watching. Start of making all database calls optionally async and in a transaction. Acquire a short wakelock while loading threads in the background. Update the notifier with peeking and remove ticker and count support. Shorten quotes and subject. --- Clover/app/src/main/AndroidManifest.xml | 50 +- .../src/main/java/org/floens/chan/Chan.java | 2 +- .../java/org/floens/chan/chan/ChanLoader.java | 38 +- .../chan/core/database/DatabaseManager.java | 162 ++-- .../core/database/DatabasePinManager.java | 102 +++ .../chan/core/manager/WatchManager.java | 751 +++++++++++++----- .../java/org/floens/chan/core/model/Pin.java | 31 - .../chan/core/presenter/ReplyPresenter.java | 2 +- .../chan/core/presenter/ThreadPresenter.java | 10 +- .../core/receiver/WatchUpdateReceiver.java | 41 + .../chan/core/settings/ChanSettings.java | 8 +- .../floens/chan/core/watch/PinWatcher.java | 197 ----- .../chan/ui/controller/DrawerController.java | 6 +- .../ui/controller/MainSettingsController.java | 14 +- .../controller/WatchSettingsController.java | 27 +- .../floens/chan/ui/service/WatchNotifier.java | 140 ++-- .../chan/ui/settings/ListSettingView.java | 33 +- Clover/app/src/main/res/values/strings.xml | 8 +- 18 files changed, 940 insertions(+), 682 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java create mode 100644 Clover/app/src/main/java/org/floens/chan/core/receiver/WatchUpdateReceiver.java delete mode 100644 Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java 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