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.
multisite
Floens 9 years ago
parent 66d3076fd7
commit 134d745bc5
  1. 50
      Clover/app/src/main/AndroidManifest.xml
  2. 2
      Clover/app/src/main/java/org/floens/chan/Chan.java
  3. 38
      Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java
  4. 162
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java
  5. 102
      Clover/app/src/main/java/org/floens/chan/core/database/DatabasePinManager.java
  6. 751
      Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
  7. 31
      Clover/app/src/main/java/org/floens/chan/core/model/Pin.java
  8. 2
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  9. 10
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  10. 41
      Clover/app/src/main/java/org/floens/chan/core/receiver/WatchUpdateReceiver.java
  11. 8
      Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
  12. 197
      Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java
  13. 6
      Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java
  14. 14
      Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
  15. 27
      Clover/app/src/main/java/org/floens/chan/ui/controller/WatchSettingsController.java
  16. 140
      Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java
  17. 33
      Clover/app/src/main/java/org/floens/chan/ui/settings/ListSettingView.java
  18. 8
      Clover/app/src/main/res/values/strings.xml

@ -15,15 +15,15 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.floens.chan" package="org.floens.chan"
xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="auto"> android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.NFC"/> <uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application <application
android:name=".ChanApplication" android:name=".ChanApplication"
@ -38,35 +38,45 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:label="@string/app_name"> android:label="@string/app_name">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW" />
<action android:name="android.nfc.action.NDEF_DISCOVERED"/> <action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/> <data android:scheme="http" />
<data android:scheme="https"/> <data android:scheme="https" />
<data android:host="4chan.org"/> <data android:host="4chan.org" />
<data android:host="www.4chan.org"/> <data android:host="www.4chan.org" />
<data android:host="boards.4chan.org"/> <data android:host="boards.4chan.org" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".test.TestActivity"/> <activity android:name=".test.TestActivity" />
<service <service
android:name=".ui.service.WatchNotifier" android:name=".ui.service.WatchNotifier"
android:exported="false"/> android:exported="false" />
<service <service
android:name=".ui.service.SavingNotification" android:name=".ui.service.SavingNotification"
android:exported="false"/> android:exported="false" />
<receiver
android:name=".core.receiver.WatchUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="org.floens.chan.intent.action.WATCHER_UPDATE" />
</intent-filter>
</receiver>
</application> </application>

@ -129,7 +129,7 @@ public class Chan extends Application {
databaseManager = new DatabaseManager(this); databaseManager = new DatabaseManager(this);
boardManager = new BoardManager(); boardManager = new BoardManager();
watchManager = new WatchManager(this); watchManager = new WatchManager();
Time.endTiming("Initializing application", startTime); Time.endTiming("Initializing application", startTime);

@ -53,7 +53,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
private ChanReaderRequest request; private ChanReaderRequest request;
private int currentTimeout = -1; private int currentTimeout = 0;
private int lastPostCount; private int lastPostCount;
private long lastLoadTime; private long lastLoadTime;
private ScheduledFuture<?> pendingFuture; private ScheduledFuture<?> pendingFuture;
@ -122,24 +122,25 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
/** /**
* Request more data * Request more data
*
* @return true if a request was started, false otherwise
*/ */
public void requestMoreData() { public boolean requestMoreData() {
clearPendingRunnable(); clearPendingRunnable();
if (loadable.isThreadMode() && request == null) { if (loadable.isThreadMode() && request == null) {
request = getData(); request = getData();
return true;
} else {
return false;
} }
} }
/** /**
* Request more data if the time left is below 0 If auto load more is * Request more data if {@link #getTimeUntilLoadMore()} is negative.
* disabled, this needs to be called manually. Otherwise this is called
* automatically when the timer hits 0.
*/ */
public void loadMoreIfTime() { public boolean loadMoreIfTime() {
if (getTimeUntilLoadMore() < 0L) { return getTimeUntilLoadMore() < 0L && requestMoreData();
requestMoreData();
}
} }
public void quickLoad() { public void quickLoad() {
@ -216,6 +217,14 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
lastLoadTime = Time.get(); lastLoadTime = Time.get();
int postCount = thread.posts.size();
if (postCount > lastPostCount) {
lastPostCount = postCount;
currentTimeout = 0;
} else {
currentTimeout = Math.min(currentTimeout + 1, watchTimeouts.length - 1);
}
for (ChanLoaderCallback l : listeners) { for (ChanLoaderCallback l : listeners) {
l.onChanLoaderData(thread); l.onChanLoaderData(thread);
} }
@ -262,15 +271,6 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
public void setTimer() { public void setTimer() {
clearPendingRunnable(); clearPendingRunnable();
int postCount = thread == null ? 0 : thread.posts.size();
if (postCount > lastPostCount) {
lastPostCount = postCount;
currentTimeout = 0;
} else {
currentTimeout++;
currentTimeout = Math.min(currentTimeout, watchTimeouts.length - 1);
}
int watchTimeout = watchTimeouts[currentTimeout]; int watchTimeout = watchTimeouts[currentTimeout];
Logger.d(TAG, "Scheduled reload in " + watchTimeout + "s"); Logger.d(TAG, "Scheduled reload in " + watchTimeout + "s");
@ -289,7 +289,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
} }
public void clearTimer() { public void clearTimer() {
currentTimeout = -1; currentTimeout = 0;
clearPendingRunnable(); clearPendingRunnable();
} }

@ -18,6 +18,8 @@
package org.floens.chan.core.database; package org.floens.chan.core.database;
import android.content.Context; import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import com.j256.ormlite.dao.Dao; import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.misc.TransactionManager; import com.j256.ormlite.misc.TransactionManager;
@ -28,7 +30,6 @@ import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.Filter; import org.floens.chan.core.model.Filter;
import org.floens.chan.core.model.History; import org.floens.chan.core.model.History;
import org.floens.chan.core.model.Loadable; 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.model.Post;
import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.model.SavedReply;
import org.floens.chan.core.model.ThreadHide; import org.floens.chan.core.model.ThreadHide;
@ -69,8 +70,25 @@ public class DatabaseManager {
private final Object historyLock = new Object(); private final Object historyLock = new Object();
private final HashMap<Loadable, History> historyByLoadable = new HashMap<>(); private final HashMap<Loadable, History> historyByLoadable = new HashMap<>();
private final DatabasePinManager databasePinManager;
public DatabaseManager(Context context) { public DatabaseManager(Context context) {
helper = new DatabaseHelper(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(); initialize();
} }
@ -127,91 +145,8 @@ public class DatabaseManager {
return getSavedReply(board, no) != null; return getSavedReply(board, no) != null;
} }
/** public DatabasePinManager getDatabasePinManager() {
* Adds a {@link Pin} to the pin table. return databasePinManager;
*
* @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<Pin> pins) {
try {
callInTransaction(helper.getConnectionSource(), new Callable<Void>() {
@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<Pin> getPinned() {
List<Pin> 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;
} }
/** /**
@ -431,20 +366,6 @@ public class DatabaseManager {
return o; 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. * Threadsafe.
*/ */
@ -541,4 +462,45 @@ public class DatabaseManager {
Logger.e(TAG, "Error trimming table " + table, e); Logger.e(TAG, "Error trimming table " + table, e);
} }
} }
public <T> void runTask(final Callable<T> taskCallable) {
runTask(taskCallable, null);
}
public <T> void runTask(final Callable<T> taskCallable, final TaskResult<T> 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> T runTaskSync(final Callable<T> taskCallable) {
try {
return TransactionManager.callInTransaction(helper.getConnectionSource(), taskCallable);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public <T> void completeTask(final TaskResult<T> task, final T result) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
task.onComplete(result);
}
});
}
public interface TaskResult<T> {
void onComplete(T result);
}
} }

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Pin> createPin(final Pin pin) {
return new Callable<Pin>() {
@Override
public Pin call() throws Exception {
helper.loadableDao.create(pin.loadable);
helper.pinDao.create(pin);
return pin;
}
};
}
public Callable<Void> deletePin(final Pin pin) {
return new Callable<Void>() {
@Override
public Void call() throws Exception {
helper.pinDao.delete(pin);
helper.loadableDao.delete(pin.loadable);
return null;
}
};
}
public Callable<Pin> updatePin(final Pin pin) {
return new Callable<Pin>() {
@Override
public Pin call() throws Exception {
helper.pinDao.update(pin);
helper.loadableDao.update(pin.loadable);
return pin;
}
};
}
public Callable<List<Pin>> updatePins(final List<Pin> pins) {
return new Callable<List<Pin>>() {
@Override
public List<Pin> 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<List<Pin>> getPins() {
return new Callable<List<Pin>>() {
@Override
public List<Pin> call() throws Exception {
List<Pin> list = helper.pinDao.queryForAll();
for (int i = 0; i < list.size(); i++) {
Pin p = list.get(i);
helper.loadableDao.refresh(p.loadable);
}
return list;
}
};
}
}

@ -17,34 +17,89 @@
*/ */
package org.floens.chan.core.manager; package org.floens.chan.core.manager;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; 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;
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.Loadable;
import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post; 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.core.settings.ChanSettings;
import org.floens.chan.ui.helper.PostHelper; import org.floens.chan.ui.helper.PostHelper;
import org.floens.chan.ui.service.WatchNotifier; import org.floens.chan.ui.service.WatchNotifier;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.concurrent.Executors; import java.util.Locale;
import java.util.concurrent.ScheduledExecutorService; import java.util.Map;
import java.util.concurrent.ScheduledFuture; import java.util.Set;
import java.util.concurrent.TimeUnit;
import de.greenrobot.event.EventBus; import de.greenrobot.event.EventBus;
import static org.floens.chan.utils.AndroidUtils.getAppContext;
/**
* Manages all Pin related management.
* <p/>
* <p>Pins are threads that are pinned to a pane on the left.
* <p/>
* <p>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.
* <p/>
* <p>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.
* <p/>
* <p>All pin adding and removing must go through this class to properly update the watchers.
*/
public class WatchManager { public class WatchManager {
private static final String TAG = "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<Pin> SORT_PINS = new Comparator<Pin>() { private static final Comparator<Pin> SORT_PINS = new Comparator<Pin>() {
@Override @Override
@ -53,74 +108,61 @@ public class WatchManager {
} }
}; };
private final Context context; private final AlarmManager alarmManager;
private final PowerManager powerManager;
private final List<Pin> pins; private final List<Pin> pins;
private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private final DatabaseManager databaseManager;
private PendingTimer pendingTimer; private final DatabasePinManager databasePinManager;
public WatchManager(Context context) { private final Handler handler;
this.context = context;
pins = Chan.getDatabaseManager().getPinned(); private IntervalType currentInterval = IntervalType.NONE;
Collections.sort(pins, SORT_PINS);
EventBus.getDefault().register(this); private Map<Pin, PinWatcher> pinWatchers = new HashMap<>();
updateTimerState(true); private Set<PinWatcher> waitingForPinWatchersForBackgroundUpdate;
updateNotificationServiceState(); private PowerManager.WakeLock wakeLock;
updatePinWatchers(); private long lastBackgroundUpdateTime;
}
/** public WatchManager() {
* Look for a pin that has an loadable that is equal to the supplied alarmManager = (AlarmManager) getAppContext().getSystemService(Context.ALARM_SERVICE);
* loadable. powerManager = (PowerManager) getAppContext().getSystemService(Context.POWER_SERVICE);
*
* @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;
}
}
return null; databaseManager = Chan.getDatabaseManager();
} databasePinManager = databaseManager.getDatabasePinManager();
pins = databaseManager.runTaskSync(databasePinManager.getPins());
Collections.sort(pins, SORT_PINS);
public Pin findPinById(int id) { handler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
for (Pin pin : pins) { @Override
if (pin.id == id) { public boolean handleMessage(Message msg) {
return pin; if (msg.what == MESSAGE_UPDATE) {
update(false);
return true;
} else {
return false;
}
} }
} });
return null; EventBus.getDefault().register(this);
}
public List<Pin> getPins() { updateState();
return pins;
} }
public List<Pin> getWatchingPins() { public boolean createPin(Loadable loadable, Post opPost) {
if (ChanSettings.watchEnabled.get()) { Pin pin = new Pin();
List<Pin> l = new ArrayList<>(); pin.loadable = loadable;
pin.loadable.title = PostHelper.getTitle(opPost, loadable);
for (Pin p : pins) { pin.thumbnailUrl = opPost.thumbnailUrl;
if (p.watching) return createPin(pin);
l.add(p);
}
return l;
} else {
return Collections.emptyList();
}
} }
public boolean addPin(Pin pin) { public boolean createPin(Pin pin) {
// No duplicates // 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)) { if (e.loadable.equals(pin.loadable)) {
return false; return false;
} }
@ -128,233 +170,377 @@ public class WatchManager {
pin.order = pins.size(); pin.order = pins.size();
pins.add(pin); pins.add(pin);
Chan.getDatabaseManager().addPin(pin); databaseManager.runTaskSync(databasePinManager.createPin(pin));
onPinsChanged(); updateState();
EventBus.getDefault().post(new PinAddedMessage(pin)); EventBus.getDefault().post(new PinAddedMessage(pin));
return true; return true;
} }
public boolean addPin(Post opPost) { public void deletePin(Pin pin) {
Pin pin = new Pin(); pins.remove(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 boolean addPin(Loadable loadable, Post opPost) { destroyPinWatcher(pin);
Pin pin = new Pin();
pin.loadable = loadable;
pin.loadable.title = PostHelper.getTitle(opPost, loadable);
pin.thumbnailUrl = opPost.thumbnailUrl;
return addPin(pin);
}
public void removePin(Pin pin) { databaseManager.runTaskSync(databasePinManager.deletePin(pin));
pins.remove(pin);
pin.destroyWatcher();
Chan.getDatabaseManager().removePin(pin);
// Update the new orders // Update the new orders
updateDatabase(); updatePinsInDatabase();
onPinsChanged(); updateState();
EventBus.getDefault().post(new PinRemovedMessage(pin)); EventBus.getDefault().post(new PinRemovedMessage(pin));
} }
/**
* Update the pin in the database
*
* @param pin
*/
public void updatePin(Pin pin) { public void updatePin(Pin pin) {
Chan.getDatabaseManager().updatePin(pin); databaseManager.runTaskSync(databasePinManager.updatePin(pin));
onPinsChanged(); updateState();
EventBus.getDefault().post(new PinChangedMessage(pin)); EventBus.getDefault().post(new PinChangedMessage(pin));
} }
/** public Pin findPinByLoadable(Loadable other) {
* Updates all the pins to the database. for (int i = 0; i < pins.size(); i++) {
*/ Pin pin = pins.get(i);
public void updateDatabase() { if (pin.loadable.equals(other)) {
Chan.getDatabaseManager().updatePins(pins); 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<Pin> getAllPins() {
return pins;
}
public List<Pin> getWatchingPins() {
if (isWatchingSettingEnabled()) {
List<Pin> 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) { public void toggleWatch(Pin pin) {
pin.watching = !pin.watching; pin.watching = !pin.watching;
updateState();
EventBus.getDefault().post(new PinChangedMessage(pin)); EventBus.getDefault().post(new PinChangedMessage(pin));
onPinsChanged();
invokeLoadNow();
} }
public void pinWatcherUpdated(Pin pin) { public void onBottomPostViewed(Pin pin) {
EventBus.getDefault().post(new PinChangedMessage(pin)); if (pin.watchNewCount >= 0) {
onPinsChanged(); pin.watchLastCount = pin.watchNewCount;
} }
if (pin.quoteNewCount >= 0) {
pin.quoteLastCount = pin.quoteNewCount;
}
PinWatcher pinWatcher = getPinWatcher(pin);
if (pinWatcher != null) {
pinWatcher.onViewed();
}
public void onPinsChanged() { updatePin(pin);
updateTimerState(false);
updateNotificationServiceState();
updatePinWatchers();
} }
public void invokeLoadNow() { // Called when the app changes foreground state
if (pendingTimer != null) { public void onEvent(Chan.ForegroundChangedMessage message) {
pendingTimer.cancel(); updateState();
pendingTimer = null; if (!message.inForeground) {
Logger.d(TAG, "Canceled timer"); 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<Pin> watchingPins = getWatchingPins(); List<Pin> watchingPins = getWatchingPins();
for (Pin pin : watchingPins) { for (int i = 0; i < watchingPins.size(); i++) {
Pin pin = watchingPins.get(i);
pin.watching = false; pin.watching = false;
} }
onPinsChanged(); updateState();
updateDatabase(); updatePinsInDatabase();
for (Pin pin : getPins()) { List<Pin> allPins = getAllPins();
for (int i = 0; i < allPins.size(); i++) {
Pin pin = allPins.get(i);
EventBus.getDefault().post(new PinChangedMessage(pin)); EventBus.getDefault().post(new PinChangedMessage(pin));
} }
} }
public void onEvent(Chan.ForegroundChangedMessage message) { // Called when the user changes the watch enabled preference
updateNotificationServiceState();
updateTimerState(true);
if (!message.inForeground) {
updateDatabase();
}
}
public void onWatchEnabledChanged(boolean watchEnabled) { public void onWatchEnabledChanged(boolean watchEnabled) {
updateNotificationServiceState(watchEnabled, getWatchBackgroundEnabled()); updateState(watchEnabled, isBackgroundWatchingSettingEnabled());
updateTimerState(watchEnabled, getWatchBackgroundEnabled(), false); List<Pin> pins = getAllPins();
updatePinWatchers(watchEnabled); for (int i = 0; i < pins.size(); i++) {
for (Pin pin : getPins()) { Pin pin = pins.get(i);
EventBus.getDefault().post(new PinChangedMessage(pin)); EventBus.getDefault().post(new PinChangedMessage(pin));
} }
} }
// Called when the user changes the watch background enabled preference
public void onBackgroundWatchingChanged(boolean backgroundEnabled) { public void onBackgroundWatchingChanged(boolean backgroundEnabled) {
updateNotificationServiceState(getTimerEnabled(), backgroundEnabled); updateState(isTimerEnabled(), backgroundEnabled);
updateTimerState(getTimerEnabled(), backgroundEnabled, false); List<Pin> pins = getAllPins();
for (Pin pin : getPins()) { for (int i = 0; i < pins.size(); i++) {
Pin pin = pins.get(i);
EventBus.getDefault().post(new PinChangedMessage(pin)); EventBus.getDefault().post(new PinChangedMessage(pin));
} }
} }
private boolean getTimerEnabled() { public PinWatcher getPinWatcher(Pin pin) {
// getWatchingPins returns an empty list when ChanPreferences.getWatchEnabled() is false return pinWatchers.get(pin);
return getWatchingPins().size() > 0;
} }
public boolean getWatchBackgroundEnabled() { private boolean createPinWatcher(Pin pin) {
return ChanSettings.watchBackground.get(); if (!pinWatchers.containsKey(pin)) {
pinWatchers.put(pin, new PinWatcher(pin));
return true;
} else {
return false;
}
} }
private void updatePinWatchers() { private boolean destroyPinWatcher(Pin pin) {
updatePinWatchers(ChanSettings.watchEnabled.get()); PinWatcher pinWatcher = pinWatchers.remove(pin);
if (pinWatcher != null) {
pinWatcher.destroy();
}
return pinWatcher != null;
} }
private void updatePinWatchers(boolean watchEnabled) { private void updatePinsInDatabase() {
for (Pin pin : pins) { databaseManager.runTask(databasePinManager.updatePins(pins));
if (watchEnabled) {
pin.createWatcher();
} else {
pin.destroyWatcher();
}
}
} }
private void updateNotificationServiceState() { private Boolean isWatchingSettingEnabled() {
updateNotificationServiceState(getTimerEnabled(), getWatchBackgroundEnabled()); return ChanSettings.watchEnabled.get();
} }
private void updateNotificationServiceState(boolean watchEnabled, boolean backgroundEnabled) { private boolean isBackgroundWatchingSettingEnabled() {
if (watchEnabled && backgroundEnabled) { return ChanSettings.watchBackground.get();
// Also calls onStartCommand, which updates the notification }
context.startService(new Intent(context, WatchNotifier.class));
} else { private boolean isInForeground() {
context.stopService(new Intent(context, WatchNotifier.class)); return Chan.getInstance().getApplicationInForeground();
} }
private boolean isTimerEnabled() {
return !getWatchingPins().isEmpty();
}
private int getBackgroundIntervalSetting() {
return ChanSettings.watchBackgroundInterval.get();
} }
private void updateTimerState(boolean invokeLoadNow) { private void updateState() {
updateTimerState(getTimerEnabled(), getWatchBackgroundEnabled(), invokeLoadNow); updateState(isTimerEnabled(), isBackgroundWatchingSettingEnabled());
} }
private void updateTimerState(boolean watchEnabled, boolean backgroundEnabled, boolean invokeLoadNow) { // Update the interval type, according to the current settings,
Logger.d(TAG, "updateTimerState watchEnabled=" + watchEnabled + " backgroundEnabled=" + backgroundEnabled + " invokeLoadNow=" + invokeLoadNow + " foreground=" + Chan.getInstance().getApplicationInForeground()); // creates and destroys the PinWatchers where needed and
if (watchEnabled) { // updates the notification.
if (Chan.getInstance().getApplicationInForeground()) { private void updateState(boolean watchEnabled, boolean backgroundEnabled) {
setTimer(invokeLoadNow ? 1 : FOREGROUND_TIME); Logger.d(TAG, "updateState watchEnabled=" + watchEnabled + " backgroundEnabled=" + backgroundEnabled + " foreground=" + isInForeground());
IntervalType intervalType;
if (!watchEnabled) {
intervalType = IntervalType.NONE;
} else {
if (isInForeground()) {
intervalType = IntervalType.FOREGROUND;
} else { } else {
if (backgroundEnabled) { if (backgroundEnabled) {
setTimer(Integer.parseInt(ChanSettings.watchBackgroundTimeout.get())); intervalType = IntervalType.BACKGROUND;
} else { } else {
if (pendingTimer != null) { intervalType = IntervalType.NONE;
pendingTimer.cancel();
pendingTimer = null;
Logger.d(TAG, "Canceled timer");
}
} }
} }
} else { }
if (pendingTimer != null) {
pendingTimer.cancel(); // Changing interval type, like when watching is disabled or the app goes to the background
pendingTimer = null; if (currentInterval != intervalType) {
Logger.d(TAG, "Canceled timer"); // 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) { // Schedule a broadcast that calls WatchUpdateReceiver.onReceive() if enabled is true,
if (pendingTimer != null && pendingTimer.time == time) { // and unschedules it when false
return; 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) { switch (currentInterval) {
pendingTimer.cancel(); case FOREGROUND:
pendingTimer = null; // reschedule handler message
Logger.d(TAG, "Canceled timer"); handler.sendMessageDelayed(handler.obtainMessage(MESSAGE_UPDATE), FOREGROUND_INTERVAL);
break;
case BACKGROUND:
// AlarmManager is scheduled as an interval
break;
} }
ScheduledFuture scheduledFuture = executor.schedule(new Runnable() { // A set of watchers that all have to complete being updated
@Override // before the wakelock is released again
public void run() { waitingForPinWatchersForBackgroundUpdate = null;
AndroidUtils.runOnUiThread(new Runnable() { if (fromBackground) {
@Override waitingForPinWatchersForBackgroundUpdate = new HashSet<>();
public void run() { }
timerFired();
} List<Pin> 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() { private void pinWatcherUpdated(PinWatcher pinWatcher) {
Logger.d(TAG, "Timer fired"); updateState();
pendingTimer = null; EventBus.getDefault().post(new PinChangedMessage(pinWatcher.pin));
for (Pin pin : getWatchingPins()) { if (waitingForPinWatchersForBackgroundUpdate != null) {
if (pin.update()) { waitingForPinWatchersForBackgroundUpdate.remove(pinWatcher);
EventBus.getDefault().post(new PinChangedMessage(pin));
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 { public static class PinAddedMessage {
@ -381,17 +567,172 @@ public class WatchManager {
} }
} }
private static class PendingTimer { public class PinWatcher implements ChanLoader.ChanLoaderCallback {
public ScheduledFuture scheduledFuture; private static final String TAG = "PinWatcher";
public int time;
private final Pin pin;
private ChanLoader chanLoader;
private final List<Post> posts = new ArrayList<>();
private final List<Post> 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) { public List<Post> getUnviewedPosts() {
this.scheduledFuture = scheduledFuture; if (posts.size() == 0) {
this.time = time; return posts;
} else {
return posts.subList(Math.max(0, posts.size() - pin.getNewPostCount()), posts.size());
}
} }
public void cancel() { public List<Post> getUnviewedQuotes() {
scheduledFuture.cancel(false); 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<Post> 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);
} }
} }
} }

@ -20,8 +20,6 @@ package org.floens.chan.core.model;
import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable; import com.j256.ormlite.table.DatabaseTable;
import org.floens.chan.core.watch.PinWatcher;
@DatabaseTable @DatabaseTable
public class Pin { public class Pin {
@DatabaseField(generatedId = true) @DatabaseField(generatedId = true)
@ -57,8 +55,6 @@ public class Pin {
@DatabaseField @DatabaseField
public boolean archived = false; public boolean archived = false;
private PinWatcher pinWatcher;
public int getNewPostCount() { public int getNewPostCount() {
if (watchLastCount < 0 || watchNewCount < 0) { if (watchLastCount < 0 || watchNewCount < 0) {
return 0; return 0;
@ -74,31 +70,4 @@ public class Pin {
return Math.max(0, quoteNewCount - quoteLastCount); 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;
}
}
} }

@ -211,7 +211,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>,
if (ChanSettings.postPinThread.get() && loadable.isThreadMode()) { if (ChanSettings.postPinThread.get() && loadable.isThreadMode()) {
ChanThread thread = callback.getThread(); ChanThread thread = callback.getThread();
if (thread != null) { if (thread != null) {
watchManager.addPin(loadable, thread.op); watchManager.createPin(loadable, thread.op);
} }
} }

@ -162,10 +162,10 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
if (pin == null) { if (pin == null) {
if (chanLoader.getThread() != null) { if (chanLoader.getThread() != null) {
Post op = chanLoader.getThread().op; Post op = chanLoader.getThread().op;
watchManager.addPin(loadable, op); watchManager.createPin(loadable, op);
} }
} else { } else {
watchManager.removePin(pin); watchManager.deletePin(pin);
} }
return isPinned(); return isPinned();
} }
@ -275,8 +275,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
Pin pin = watchManager.findPinByLoadable(loadable); Pin pin = watchManager.findPinByLoadable(loadable);
if (pin != null) { if (pin != null) {
pin.onBottomPostViewed(); watchManager.onBottomPostViewed(pin);
watchManager.updatePin(pin);
} }
threadPresenterCallback.showNewPostsNotification(false, -1); threadPresenterCallback.showNewPostsNotification(false, -1);
@ -449,7 +448,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
databaseManager.saveReply(new SavedReply(post.board, post.no, "foo")); databaseManager.saveReply(new SavedReply(post.board, post.no, "foo"));
break; break;
case POST_OPTION_PIN: case POST_OPTION_PIN:
watchManager.addPin(post); Loadable pinLoadable = LoadablePool.getInstance().obtain(new Loadable(post.board, post.no));
watchManager.createPin(pinLoadable, post);
break; break;
case POST_OPTION_OPEN_BROWSER: case POST_OPTION_OPEN_BROWSER:
AndroidUtils.openLink( AndroidUtils.openLink(

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

@ -24,6 +24,7 @@ import android.text.TextUtils;
import org.floens.chan.Chan; import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls; 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.adapter.PostsFilter;
import org.floens.chan.ui.cell.PostCellInterface; import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
@ -100,9 +101,10 @@ public class ChanSettings {
public static final BooleanSetting watchEnabled; public static final BooleanSetting watchEnabled;
public static final BooleanSetting watchCountdown; public static final BooleanSetting watchCountdown;
public static final BooleanSetting watchBackground; public static final BooleanSetting watchBackground;
public static final StringSetting watchBackgroundTimeout; public static final IntegerSetting watchBackgroundInterval;
public static final StringSetting watchNotifyMode; public static final StringSetting watchNotifyMode;
public static final StringSetting watchSound; public static final StringSetting watchSound;
public static final BooleanSetting watchPeek;
public static final StringSetting watchLed; public static final StringSetting watchLed;
public static final StringSetting passToken; public static final StringSetting passToken;
@ -184,9 +186,10 @@ public class ChanSettings {
Chan.getWatchManager().onBackgroundWatchingChanged(value); 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"); watchNotifyMode = new StringSetting(p, "preference_watch_notify_mode", "all");
watchSound = new StringSetting(p, "preference_watch_sound", "quotes"); watchSound = new StringSetting(p, "preference_watch_sound", "quotes");
watchPeek = new BooleanSetting(p, "preference_watch_peek", true);
watchLed = new StringSetting(p, "preference_watch_led", "ffffffff"); watchLed = new StringSetting(p, "preference_watch_led", "ffffffff");
passToken = new StringSetting(p, "preference_pass_token", ""); passToken = new StringSetting(p, "preference_pass_token", "");
@ -227,6 +230,7 @@ public class ChanSettings {
// preference_board_editor_filler default false // preference_board_editor_filler default false
// preference_pass_enabled default false // preference_pass_enabled default false
// preference_autoplay false // preference_autoplay false
// preference_watch_background_timeout "60" the old timeout background setting in minutes
} }
public static boolean passLoggedIn() { public static boolean passLoggedIn() {

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Post> posts = new ArrayList<>();
private final List<Post> 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<Post> getUnviewedPosts() {
if (posts.size() == 0) {
return posts;
} else {
return posts.subList(Math.max(0, posts.size() - pin.getNewPostCount()), posts.size());
}
}
public List<Post> 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<Post> 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);
}
}

@ -90,7 +90,7 @@ public class DrawerController extends Controller implements PinAdapter.Callback,
pinAdapter = new PinAdapter(this); pinAdapter = new PinAdapter(this);
recyclerView.setAdapter(pinAdapter); recyclerView.setAdapter(pinAdapter);
pinAdapter.onPinsChanged(watchManager.getPins()); pinAdapter.onPinsChanged(watchManager.getAllPins());
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(pinAdapter.getItemTouchHelperCallback()); ItemTouchHelper itemTouchHelper = new ItemTouchHelper(pinAdapter.getItemTouchHelperCallback());
itemTouchHelper.attachToRecyclerView(recyclerView); itemTouchHelper.attachToRecyclerView(recyclerView);
@ -157,14 +157,14 @@ public class DrawerController extends Controller implements PinAdapter.Callback,
@Override @Override
public void onPinRemoved(final Pin pin) { 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); Snackbar snackbar = Snackbar.make(drawerLayout, context.getString(R.string.drawer_pin_removed, pin.loadable.title), Snackbar.LENGTH_LONG);
fixSnackbarText(context, snackbar); fixSnackbarText(context, snackbar);
snackbar.setAction(R.string.undo, new View.OnClickListener() { snackbar.setAction(R.string.undo, new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
watchManager.addPin(pin); watchManager.createPin(pin);
} }
}); });
snackbar.show(); snackbar.show();

@ -56,8 +56,8 @@ import static org.floens.chan.utils.AndroidUtils.getString;
public class MainSettingsController extends SettingsController implements ToolbarMenuItem.ToolbarMenuItemCallback, WatchSettingsController.WatchSettingControllerListener, PassSettingsController.PassSettingControllerListener { public class MainSettingsController extends SettingsController implements ToolbarMenuItem.ToolbarMenuItemCallback, WatchSettingsController.WatchSettingControllerListener, PassSettingsController.PassSettingControllerListener {
private static final int ADVANCED_SETTINGS = 1; private static final int ADVANCED_SETTINGS = 1;
private ListSettingView imageAutoLoadView; private ListSettingView<String> imageAutoLoadView;
private ListSettingView videoAutoLoadView; private ListSettingView<String> videoAutoLoadView;
private LinkSettingView watchLink; private LinkSettingView watchLink;
private LinkSettingView passLink; private LinkSettingView passLink;
@ -182,7 +182,7 @@ public class MainSettingsController extends SettingsController implements Toolba
fontSizes.add(new ListSettingView.Item(name, String.valueOf(size))); 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)); fontCondensed = appearance.add(new BooleanSettingView(this, ChanSettings.fontCondensed, R.string.setting_font_condensed, R.string.setting_font_condensed_description));
groups.add(appearance); groups.add(appearance);
@ -215,13 +215,13 @@ public class MainSettingsController extends SettingsController implements Toolba
break; break;
} }
imageAutoLoadTypes.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)); 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); 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); browsing.add(videoAutoLoadView);
updateVideoLoadModes(); updateVideoLoadModes();

@ -39,6 +39,7 @@ public class WatchSettingsController extends SettingsController implements Compo
private SettingView backgroundTimeout; private SettingView backgroundTimeout;
private SettingView notifyMode; private SettingView notifyMode;
private SettingView soundMode; private SettingView soundMode;
private SettingView peekMode;
private SettingView ledMode; private SettingView ledMode;
public WatchSettingsController(Context context) { public WatchSettingsController(Context context) {
@ -72,6 +73,7 @@ public class WatchSettingsController extends SettingsController implements Compo
setSettingViewVisibility(backgroundTimeout, false, false); setSettingViewVisibility(backgroundTimeout, false, false);
setSettingViewVisibility(notifyMode, false, false); setSettingViewVisibility(notifyMode, false, false);
setSettingViewVisibility(soundMode, false, false); setSettingViewVisibility(soundMode, false, false);
setSettingViewVisibility(peekMode, false, false);
setSettingViewVisibility(ledMode, false, false); setSettingViewVisibility(ledMode, false, false);
} }
} }
@ -94,6 +96,7 @@ public class WatchSettingsController extends SettingsController implements Compo
setSettingViewVisibility(backgroundTimeout, enabled, true); setSettingViewVisibility(backgroundTimeout, enabled, true);
setSettingViewVisibility(notifyMode, enabled, true); setSettingViewVisibility(notifyMode, enabled, true);
setSettingViewVisibility(soundMode, enabled, true); setSettingViewVisibility(soundMode, enabled, true);
setSettingViewVisibility(peekMode, enabled, true);
setSettingViewVisibility(ledMode, 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))); // 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)); 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]; ListSettingView.Item[] timeoutsItems = new ListSettingView.Item[timeouts.length];
for (int i = 0; i < timeouts.length; i++) { for (int i = 0; i < timeouts.length; i++) {
String name = context.getResources().getQuantityString(R.plurals.minutes, timeouts[i], timeouts[i]); int value = timeouts[i] / 1000 / 60;
timeoutsItems[i] = new ListSettingView.Item(name, String.valueOf(timeouts[i] * 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<Integer>(this, ChanSettings.watchBackgroundInterval, R.string.setting_watch_background_timeout, timeoutsItems) {
@Override @Override
public String getBottomDescription() { public String getBottomDescription() {
return getString(R.string.setting_watch_background_timeout_description) + "\n\n" + items.get(selected).name; 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"})); 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"})); 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), context.getResources().getStringArray(R.array.setting_watch_leds),
new String[]{"-1", "ffffffff", "ffff0000", "ffffff00", "ff00ff00", "ff00ffff", "ff0000ff", "ffff00ff"})); new String[]{"-1", "ffffffff", "ffff0000", "ffffff00", "ff00ff00", "ff00ffff", "ff0000ff", "ffff00ff"}));

@ -25,6 +25,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.IBinder; import android.os.IBinder;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import org.floens.chan.Chan; import org.floens.chan.Chan;
import org.floens.chan.R; 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.Pin;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.settings.ChanSettings; 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.ui.activity.BoardActivity;
import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
public class WatchNotifier extends Service { public class WatchNotifier extends Service {
private static final String TAG = "WatchNotifier"; private static final String TAG = "WatchNotifier";
private static final int NOTIFICATION_ID = 1; 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 NotificationManager nm;
private WatchManager wm; private WatchManager wm;
@ -86,49 +89,54 @@ public class WatchNotifier extends Service {
} }
public void pausePins() { public void pausePins() {
wm.pausePins(); wm.pauseAll();
} }
private Notification createNotification() { private Notification createNotification() {
boolean notifyQuotesOnly = ChanSettings.watchNotifyMode.get().equals("quotes"); boolean notifyQuotesOnly = ChanSettings.watchNotifyMode.get().equals("quotes");
boolean soundQuotesOnly = ChanSettings.watchSound.get().equals("quotes"); boolean soundQuotesOnly = ChanSettings.watchSound.get().equals("quotes");
List<Post> list = new ArrayList<>(); List<Post> unviewedPosts = new ArrayList<>();
List<Post> listQuoting = new ArrayList<>(); List<Post> listQuoting = new ArrayList<>();
List<Pin> pins = new ArrayList<>(); List<Pin> pins = new ArrayList<>();
List<Pin> subjectPins = new ArrayList<>(); List<Pin> subjectPins = new ArrayList<>();
boolean ticker = false; boolean light = false;
boolean sound = false; boolean sound = false;
boolean peek = false;
for (Pin pin : wm.getWatchingPins()) { for (Pin pin : wm.getWatchingPins()) {
PinWatcher watcher = pin.getPinWatcher(); WatchManager.PinWatcher watcher = wm.getPinWatcher(pin);
if (watcher == null || pin.isError) if (watcher == null || pin.isError) {
continue; continue;
}
pins.add(pin); pins.add(pin);
if (notifyQuotesOnly) { if (notifyQuotesOnly) {
list.addAll(watcher.getUnviewedQuotes()); unviewedPosts.addAll(watcher.getUnviewedQuotes());
listQuoting.addAll(watcher.getUnviewedQuotes()); listQuoting.addAll(watcher.getUnviewedQuotes());
if (watcher.getWereNewQuotes()) { if (watcher.getWereNewQuotes()) {
ticker = true; light = true;
sound = true; sound = true;
peek = true;
} }
if (pin.getNewQuoteCount() > 0) { if (pin.getNewQuoteCount() > 0) {
subjectPins.add(pin); subjectPins.add(pin);
} }
} else { } else {
list.addAll(watcher.getUnviewedPosts()); unviewedPosts.addAll(watcher.getUnviewedPosts());
listQuoting.addAll(watcher.getUnviewedQuotes()); listQuoting.addAll(watcher.getUnviewedQuotes());
if (watcher.getWereNewPosts()) { if (watcher.getWereNewPosts()) {
ticker = true; light = true;
if (!soundQuotesOnly) { if (!soundQuotesOnly) {
sound = true; sound = true;
peek = true;
} }
} }
if (watcher.getWereNewQuotes()) { if (watcher.getWereNewQuotes()) {
sound = true; sound = true;
peek = true;
} }
if (pin.getNewPostCount() > 0) { if (pin.getNewPostCount() > 0) {
subjectPins.add(pin); subjectPins.add(pin);
@ -137,63 +145,69 @@ public class WatchNotifier extends Service {
} }
if (Chan.getInstance().getApplicationInForeground()) { if (Chan.getInstance().getApplicationInForeground()) {
ticker = false; light = false;
sound = 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<Pin> pins, List<Pin> subjectPins, List<Post> list, List<Post> listQuoting, private Notification notifyAboutPosts(List<Pin> pins, List<Pin> subjectPins, List<Post> unviewedPosts, List<Post> listQuoting,
boolean notifyQuotesOnly, boolean makeTicker, boolean makeSound) { boolean notifyQuotesOnly, boolean light, boolean sound, boolean peek) {
String title = getResources().getQuantityString(R.plurals.watch_title, pins.size(), pins.size()); String title = getResources().getQuantityString(R.plurals.watch_title, pins.size(), pins.size());
if (list.size() == 0) { if (unviewedPosts.isEmpty()) {
// Idle notification // Idle notification
String message = getString(R.string.watch_idle); 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 { } else {
// New posts notification // New posts notification
String message; String message;
List<Post> notificationList; List<Post> postsForExpandedLines;
if (notifyQuotesOnly) { if (notifyQuotesOnly) {
message = getResources().getQuantityString(R.plurals.watch_new_quotes, listQuoting.size(), listQuoting.size()); message = getResources().getQuantityString(R.plurals.watch_new_quotes, listQuoting.size(), listQuoting.size());
notificationList = listQuoting; postsForExpandedLines = listQuoting;
} else { } else {
notificationList = list; postsForExpandedLines = unviewedPosts;
if (listQuoting.size() > 0) { 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 { } 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); Collections.sort(postsForExpandedLines, POST_AGE_COMPARATOR);
List<CharSequence> lines = new ArrayList<>(); List<CharSequence> expandedLines = new ArrayList<>();
for (Post post : notificationList) { for (Post postForExpandedLine : postsForExpandedLines) {
CharSequence prefix = AndroidUtils.ellipsize(post.title, 18); CharSequence prefix;
if (postForExpandedLine.title.length() <= SUBJECT_LENGTH) {
CharSequence comment; prefix = postForExpandedLine.title;
if (post.comment.length() == 0) {
comment = "(image)";
} else { } 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; // Replace >>132456798 with >789 to shorten the notification
if (subjectPins.size() == 1) { comment = SHORTEN_NO_PATTERN.matcher(comment).replaceAll(">$1");
subject = subjectPins.get(0);
expandedLines.add(prefix + ": " + comment);
} }
String ticker = null; Pin targetPin = null;
if (makeTicker) { if (subjectPins.size() == 1) {
ticker = message; 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. * Create a notification with the supplied parameters.
* The style of the big notification is InboxStyle, a list of text. * 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 title The title of the notification
* @param contentTitle The title of the notification * @param smallText The content of the small notification
* @param contentText The content of the small notification
* @param contentNumber The contentInfo and number, or -1 if not shown
* @param expandedLines A list of lines for the big notification, or null if not shown * @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 * @param target The target pin, or null to open the pinned pane on tap
*/ */
@SuppressWarnings("deprecation") private Notification get(String title, String smallText, List<CharSequence> expandedLines,
private Notification getNotificationFor(String tickerText, String contentTitle, String contentText, int contentNumber, boolean light, boolean sound, boolean peek, boolean alertIcon, Pin target) {
List<CharSequence> expandedLines, boolean light, boolean makeSound, boolean alert, Pin target) {
Intent intent = new Intent(this, BoardActivity.class); Intent intent = new Intent(this, BoardActivity.class);
intent.setAction(Intent.ACTION_MAIN); intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER); 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); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this); NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
if (makeSound) { if (sound || peek) {
builder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE); builder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
} }
@ -234,24 +247,21 @@ public class WatchNotifier extends Service {
} }
} }
builder.setContentIntent(pendingIntent); builder.setContentIntent(pendingIntent);
if (tickerText != null) { builder.setContentTitle(title);
tickerText = tickerText.substring(0, Math.min(tickerText.length(), 50)); if (smallText != null) {
builder.setContentText(smallText);
} }
builder.setTicker(tickerText); if (alertIcon || peek) {
builder.setContentTitle(contentTitle); builder.setSmallIcon(R.drawable.ic_stat_notify_alert);
builder.setContentText(contentText); builder.setPriority(NotificationCompat.PRIORITY_HIGH);
} else {
if (contentNumber >= 0) { builder.setSmallIcon(R.drawable.ic_stat_notify);
builder.setContentInfo(Integer.toString(contentNumber)); builder.setPriority(NotificationCompat.PRIORITY_MIN);
builder.setNumber(contentNumber);
} }
builder.setSmallIcon(alert ? R.drawable.ic_stat_notify_alert : R.drawable.ic_stat_notify);
Intent pauseWatching = new Intent(this, WatchNotifier.class); Intent pauseWatching = new Intent(this, WatchNotifier.class);
pauseWatching.putExtra("pause_pins", true); 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())) { for (CharSequence line : expandedLines.subList(Math.max(0, expandedLines.size() - 10), expandedLines.size())) {
style.addLine(line); style.addLine(line);
} }
style.setBigContentTitle(contentTitle); style.setBigContentTitle(title);
builder.setStyle(style); builder.setStyle(style);
} }
return builder.getNotification(); return builder.build();
} }
private static class PostAgeComparer implements Comparator<Post> { private static class PostAgeComparator implements Comparator<Post> {
@Override @Override
public int compare(Post lhs, Post rhs) { public int compare(Post lhs, Post rhs) {
if (lhs.time < rhs.time) { if (lhs.time < rhs.time) {

@ -31,43 +31,43 @@ import java.util.List;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
public class ListSettingView extends SettingView implements FloatingMenu.FloatingMenuCallback, View.OnClickListener { public class ListSettingView<T> extends SettingView implements FloatingMenu.FloatingMenuCallback, View.OnClickListener {
public final List<Item> items; public final List<Item> items;
public int selected; public int selected;
private Setting<String> setting; private Setting<T> setting;
public ListSettingView(SettingsController settingsController, Setting<String> setting, int name, String[] itemNames, String[] keys) { public ListSettingView(SettingsController settingsController, Setting<T> setting, int name, String[] itemNames, String[] keys) {
this(settingsController, setting, getString(name), itemNames, keys); this(settingsController, setting, getString(name), itemNames, keys);
} }
public ListSettingView(SettingsController settingsController, Setting<String> setting, String name, String[] itemNames, String[] keys) { public ListSettingView(SettingsController settingsController, Setting<T> setting, String name, String[] itemNames, String[] keys) {
super(settingsController, name); super(settingsController, name);
this.setting = setting; this.setting = setting;
items = new ArrayList<>(itemNames.length); items = new ArrayList<>(itemNames.length);
for (int i = 0; i < itemNames.length; i++) { 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(); updateSelection();
} }
public ListSettingView(SettingsController settingsController, Setting<String> setting, int name, Item[] items) { public ListSettingView(SettingsController settingsController, Setting<T> setting, int name, Item[] items) {
this(settingsController, setting, getString(name), items); this(settingsController, setting, getString(name), items);
} }
public ListSettingView(SettingsController settingsController, Setting<String> setting, int name, List<Item> items) { public ListSettingView(SettingsController settingsController, Setting<T> setting, int name, List<Item> items) {
this(settingsController, setting, getString(name), items); this(settingsController, setting, getString(name), items);
} }
public ListSettingView(SettingsController settingsController, Setting<String> setting, String name, Item[] items) { public ListSettingView(SettingsController settingsController, Setting<T> setting, String name, Item[] items) {
this(settingsController, setting, name, Arrays.asList(items)); this(settingsController, setting, name, Arrays.asList(items));
} }
public ListSettingView(SettingsController settingsController, Setting<String> setting, String name, List<Item> items) { public ListSettingView(SettingsController settingsController, Setting<T> setting, String name, List<Item> items) {
super(settingsController, name); super(settingsController, name);
this.setting = setting; this.setting = setting;
this.items = items; this.items = items;
@ -79,7 +79,7 @@ public class ListSettingView extends SettingView implements FloatingMenu.Floatin
return items.get(selected).name; return items.get(selected).name;
} }
public Setting<String> getSetting() { public Setting<T> getSetting() {
return setting; return setting;
} }
@ -114,9 +114,10 @@ public class ListSettingView extends SettingView implements FloatingMenu.Floatin
menu.show(); menu.show();
} }
@SuppressWarnings("unchecked")
@Override @Override
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) { public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
String selectedKey = (String) item.getId(); T selectedKey = (T) item.getId();
setting.set(selectedKey); setting.set(selectedKey);
updateSelection(); updateSelection();
settingsController.onPreferenceChange(this); settingsController.onPreferenceChange(this);
@ -127,7 +128,7 @@ public class ListSettingView extends SettingView implements FloatingMenu.Floatin
} }
public void updateSelection() { public void updateSelection() {
String selectedKey = setting.get(); T selectedKey = setting.get();
for (int i = 0; i < items.size(); i++) { for (int i = 0; i < items.size(); i++) {
if (items.get(i).key.equals(selectedKey)) { if (items.get(i).key.equals(selectedKey)) {
selected = i; selected = i;
@ -136,18 +137,18 @@ public class ListSettingView extends SettingView implements FloatingMenu.Floatin
} }
} }
public static class Item { public static class Item<T> {
public final String name; public final String name;
public final String key; public final T key;
public boolean enabled; public boolean enabled;
public Item(String name, String key) { public Item(String name, T key) {
this.name = name; this.name = name;
this.key = key; this.key = key;
enabled = true; enabled = true;
} }
public Item(String name, String key, boolean enabled) { public Item(String name, T key, boolean enabled) {
this.name = name; this.name = name;
this.key = key; this.key = key;
this.enabled = enabled; this.enabled = enabled;

@ -400,18 +400,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="setting_watch_summary_disabled">Off</string> <string name="setting_watch_summary_disabled">Off</string>
<string name="setting_watch_enable_background">Enable in the background</string> <string name="setting_watch_enable_background">Enable in the background</string>
<string name="setting_watch_enable_background_description">Watch pins when Clover is in the background</string> <string name="setting_watch_enable_background_description">Watch pins when Clover is in the background</string>
<string name="setting_watch_background_timeout">Minimum time between loads in background</string> <string name="setting_watch_background_timeout">Background update interval</string>
<string name="setting_watch_background_timeout_description">Minimum time between loads, with exponential backoff</string> <string name="setting_watch_background_timeout_description">The time between updates in the background</string>
<string name="setting_watch_notify_mode">Notify about</string> <string name="setting_watch_notify_mode">Notify about</string>
<string-array name="setting_watch_notify_modes"> <string-array name="setting_watch_notify_modes">
<item>All posts</item> <item>All posts</item>
<item>Only posts quoting you</item> <item>Only posts quoting you</item>
</string-array> </string-array>
<string name="setting_watch_sound">Notification sound and vibrate</string> <string name="setting_watch_sound">Notification sound</string>
<string-array name="setting_watch_sounds"> <string-array name="setting_watch_sounds">
<item>All posts</item> <item>All posts</item>
<item>Only posts quoting you</item> <item>Only posts quoting you</item>
</string-array> </string-array>
<string name="setting_watch_peek">Heads-up notification on mentions</string>
<string name="setting_watch_peek_description">Show a heads-up notification on mentions</string>
<string name="setting_watch_led">Notification light</string> <string name="setting_watch_led">Notification light</string>
<string-array name="setting_watch_leds"> <string-array name="setting_watch_leds">
<item>None</item> <item>None</item>

Loading…
Cancel
Save