watcher: use MessagingStyle for notifications.

Use notification channels.
Stop using a foreground service.
feature/sentry
Floens 6 years ago
parent 8dac5e7b4c
commit 327489f844
  1. 108
      Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
  2. 16
      Clover/app/src/main/java/org/floens/chan/ui/notification/NotificationHelper.java
  3. 180
      Clover/app/src/main/java/org/floens/chan/ui/notification/ThreadWatchNotifications.java
  4. 19
      Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java

@ -21,11 +21,16 @@ import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.ImageLoader;
import org.floens.chan.Chan;
import org.floens.chan.core.database.DatabaseManager;
@ -40,7 +45,7 @@ import org.floens.chan.core.pool.ChanLoaderFactory;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.loader.ChanThreadLoader;
import org.floens.chan.ui.helper.PostHelper;
import org.floens.chan.ui.service.WatchNotifier;
import org.floens.chan.ui.notification.ThreadWatchNotifications;
import org.floens.chan.utils.Logger;
import java.util.ArrayList;
@ -59,6 +64,7 @@ import javax.inject.Singleton;
import de.greenrobot.event.EventBus;
import static org.floens.chan.Chan.inject;
import static org.floens.chan.Chan.injector;
import static org.floens.chan.utils.AndroidUtils.getAppContext;
/**
@ -103,7 +109,7 @@ public class WatchManager {
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 String WAKELOCK_TAG = "org.floens.chan:watch_manager_update_lock";
private static final long WAKELOCK_MAX_TIME = 60 * 1000;
private static final long BACKGROUND_UPDATE_MIN_DELAY = 90 * 1000;
@ -133,13 +139,18 @@ public class WatchManager {
private PowerManager.WakeLock wakeLock;
private long lastBackgroundUpdateTime;
private ThreadWatchNotifications threadWatchNotifications;
@Inject
public WatchManager(DatabaseManager databaseManager, ChanLoaderFactory chanLoaderFactory) {
alarmManager = (AlarmManager) getAppContext().getSystemService(Context.ALARM_SERVICE);
powerManager = (PowerManager) getAppContext().getSystemService(Context.POWER_SERVICE);
public WatchManager(Context applicationContext,
DatabaseManager databaseManager, ChanLoaderFactory chanLoaderFactory,
ThreadWatchNotifications threadWatchNotifications) {
alarmManager = (AlarmManager) applicationContext.getSystemService(Context.ALARM_SERVICE);
powerManager = (PowerManager) applicationContext.getSystemService(Context.POWER_SERVICE);
this.databaseManager = databaseManager;
this.chanLoaderFactory = chanLoaderFactory;
this.threadWatchNotifications = threadWatchNotifications;
databasePinManager = databaseManager.getDatabasePinManager();
pins = databaseManager.runTask(databasePinManager.getPins());
@ -543,10 +554,18 @@ public class WatchManager {
// Update notification state
if (watchEnabled && backgroundEnabled) {
// Also calls onStartCommand, which updates the notification with new info
getAppContext().startService(new Intent(getAppContext(), WatchNotifier.class));
// Show/update notification
List<PinWatcher> pinWatchers = new ArrayList<>();
for (Pin watchingPin : getWatchingPins()) {
PinWatcher pinWatcher = getPinWatcher(watchingPin);
if (pinWatcher != null && !watchingPin.isError) {
pinWatchers.add(pinWatcher);
}
}
threadWatchNotifications.showForWatchers(pinWatchers);
} else {
getAppContext().stopService(new Intent(getAppContext(), WatchNotifier.class));
threadWatchNotifications.hide();
}
}
@ -668,9 +687,12 @@ public class WatchManager {
}
}
public class PinWatcher implements ChanThreadLoader.ChanLoaderCallback {
public class PinWatcher implements ChanThreadLoader.ChanLoaderCallback, ImageLoader.ImageListener {
private static final String TAG = "PinWatcher";
// Width and height of the bitmap for the notification image.
private static final int THUMBNAIL_SIZE = 128;
private final Pin pin;
private ChanThreadLoader chanLoader;
@ -679,6 +701,11 @@ public class WatchManager {
private boolean wereNewQuotes = false;
private boolean wereNewPosts = false;
private boolean requireNotificationUpdate = true;
private Bitmap thumbnailBitmap = null;
private ImageLoader.ImageContainer thumbnailContainer;
public PinWatcher(Pin pin) {
this.pin = pin;
inject(this);
@ -687,6 +714,27 @@ public class WatchManager {
chanLoader = chanLoaderFactory.obtain(pin.loadable, this);
}
public int getPinId() {
return pin.id;
}
public String getTitle() {
return pin.loadable.title;
}
public boolean requiresNotificationUpdate() {
return requireNotificationUpdate;
}
public void hadNotificationUpdate() {
requireNotificationUpdate = false;
}
@Nullable
public Bitmap getThumbnailBitmap() {
return thumbnailBitmap;
}
public List<Post> getUnviewedPosts() {
if (posts.size() == 0) {
return posts;
@ -728,10 +776,13 @@ public class WatchManager {
private void onViewed() {
wereNewPosts = false;
wereNewQuotes = false;
requireNotificationUpdate = true;
}
private boolean update(boolean fromBackground) {
if (!pin.isError && pin.watching) {
loadThumbnailBitmapIfNeeded();
if (fromBackground) {
// Always load regardless of timer, since the time left is not accurate for 15min+ intervals
chanLoader.clearTimer();
@ -774,20 +825,20 @@ public class WatchManager {
quotes.clear();
// Get list of saved replies from this thread
List<Post> savedReplies = new ArrayList<>();
Set<Integer> savedReplies = new HashSet<>();
for (Post item : thread.posts) {
// saved.title = pin.loadable.title;
if (item.isSavedReply) {
savedReplies.add(item);
savedReplies.add(item.no);
}
}
// Now get a list of posts that have a quote to a saved reply
out:
for (Post post : thread.posts) {
for (Post saved : savedReplies) {
if (post.repliesTo.contains(saved.no)) {
for (Integer no : post.repliesTo) {
if (savedReplies.contains(no)) {
quotes.add(post);
continue out;
}
}
}
@ -801,6 +852,7 @@ public class WatchManager {
if (isFirstLoad) {
pin.watchLastCount = posts.size();
pin.quoteLastCount = quotes.size();
requireNotificationUpdate = true;
}
pin.watchNewCount = posts.size();
@ -810,11 +862,13 @@ public class WatchManager {
// There were new posts after processing
if (pin.watchNewCount > lastWatchNewCount) {
wereNewPosts = true;
requireNotificationUpdate = true;
}
// There were new quotes after processing
if (pin.quoteNewCount > lastQuoteNewCount) {
wereNewQuotes = true;
requireNotificationUpdate = true;
}
}
@ -828,9 +882,33 @@ public class WatchManager {
if (thread.archived || thread.closed) {
pin.archived = true;
pin.watching = false;
requireNotificationUpdate = true;
}
pinWatcherUpdated(this);
loadThumbnailBitmapIfNeeded();
}
private void loadThumbnailBitmapIfNeeded() {
if (TextUtils.isEmpty(pin.thumbnailUrl) || thumbnailContainer != null) {
return;
}
thumbnailContainer = injector().instance(ImageLoader.class)
.get(pin.thumbnailUrl, this,
THUMBNAIL_SIZE, THUMBNAIL_SIZE);
}
@Override
public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
if (response.getBitmap() != null) {
thumbnailBitmap = response.getBitmap();
}
}
@Override
public void onErrorResponse(VolleyError error) {
}
}
}

@ -0,0 +1,16 @@
package org.floens.chan.ui.notification;
import android.app.NotificationManager;
import android.content.Context;
public class NotificationHelper {
protected final Context applicationContext;
protected final NotificationManager notificationManager;
public NotificationHelper(Context applicationContext) {
this.applicationContext = applicationContext;
notificationManager = (NotificationManager) applicationContext.
getSystemService(Context.NOTIFICATION_SERVICE);
}
}

@ -0,0 +1,180 @@
package org.floens.chan.ui.notification;
import android.annotation.TargetApi;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import org.floens.chan.R;
import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.Post;
import org.floens.chan.ui.activity.BoardActivity;
import java.util.List;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class ThreadWatchNotifications extends NotificationHelper {
private static final String CHANNEL_ID_WATCH_NORMAL = "watch:normal";
private static final String CHANNEL_ID_WATCH_MENTION = "watch:mention";
private static final int NOTIFICATION_ID_WATCH_NORMAL = 0x10000;
private static final int NOTIFICATION_ID_WATCH_NORMAL_MASK = 0xffff;
private static final int NOTIFICATION_ID_WATCH_MENTION = 0x20000;
private static final int NOTIFICATION_ID_WATCH_MENTION_MASK = 0xffff;
private static final String POST_COMMENT_IMAGE_PREFIX = "(img) ";
private static final Pattern POST_COMMENT_SHORTEN_NO_PATTERN =
Pattern.compile(">>\\d+(?=\\d{4})(\\d{4})");
@Inject
public ThreadWatchNotifications(Context applicationContext) {
super(applicationContext);
}
public void showForWatchers(List<WatchManager.PinWatcher> pinWatchers) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
showPinSummaries(pinWatchers);
} else {
// legacy notification
}
}
public void hide() {
}
@TargetApi(Build.VERSION_CODES.O)
private void showPinSummaries(List<WatchManager.PinWatcher> pinWatchers) {
ensureChannels();
for (WatchManager.PinWatcher pinWatcher : pinWatchers) {
if (!pinWatcher.requiresNotificationUpdate()) {
continue;
}
// Normal thread posts.
if (!pinWatcher.getUnviewedPosts().isEmpty()) {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(applicationContext,
CHANNEL_ID_WATCH_NORMAL);
NotificationCompat.MessagingStyle messagingStyle =
new NotificationCompat.MessagingStyle("");
builder.setSmallIcon(R.drawable.ic_stat_notify);
if (pinWatcher.getThumbnailBitmap() != null) {
builder.setLargeIcon(pinWatcher.getThumbnailBitmap());
}
String subTitle = "(" + pinWatcher.getUnviewedPosts().size() + ")";
messagingStyle.setConversationTitle(pinWatcher.getTitle() + " " + subTitle);
messagingStyle.setGroupConversation(true);
addPostsToMessagingStyle(messagingStyle, pinWatcher.getUnviewedPosts());
builder.setStyle(messagingStyle);
setNotificationIntent(builder);
int id = NOTIFICATION_ID_WATCH_NORMAL +
(pinWatcher.getPinId() & NOTIFICATION_ID_WATCH_NORMAL_MASK);
notificationManager.notify(id, builder.build());
}
// Posts that mention you.
if (!pinWatcher.getUnviewedQuotes().isEmpty()) {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(applicationContext,
CHANNEL_ID_WATCH_MENTION);
NotificationCompat.MessagingStyle messagingStyle =
new NotificationCompat.MessagingStyle("");
builder.setSmallIcon(R.drawable.ic_stat_notify_alert);
builder.setSubText("Mentions");
if (pinWatcher.getThumbnailBitmap() != null) {
builder.setLargeIcon(pinWatcher.getThumbnailBitmap());
}
String subTitle = "(" + pinWatcher.getUnviewedQuotes().size() + " mentions)";
messagingStyle.setConversationTitle(pinWatcher.getTitle() + " " + subTitle);
messagingStyle.setGroupConversation(true);
addPostsToMessagingStyle(messagingStyle, pinWatcher.getUnviewedQuotes());
builder.setStyle(messagingStyle);
setNotificationIntent(builder);
int id = NOTIFICATION_ID_WATCH_MENTION +
(pinWatcher.getPinId() & NOTIFICATION_ID_WATCH_MENTION_MASK);
notificationManager.notify(id, builder.build());
}
pinWatcher.hadNotificationUpdate();
}
}
private void addPostsToMessagingStyle(NotificationCompat.MessagingStyle messagingStyle,
List<Post> unviewedPosts) {
final int maxLines = 25;
if (unviewedPosts.size() > maxLines) {
unviewedPosts = unviewedPosts.subList(
unviewedPosts.size() - maxLines, unviewedPosts.size());
}
for (Post post : unviewedPosts) {
String comment = post.image() != null ? POST_COMMENT_IMAGE_PREFIX : "";
if (post.comment.length() > 0) {
comment += post.comment;
}
// Replace >>132456798 with >6789 to shorten the notification
comment = POST_COMMENT_SHORTEN_NO_PATTERN.matcher(comment)
.replaceAll(">$1");
CharSequence name = post.nameTripcodeIdCapcodeSpan;
// if (name.length() == 0) {
// name = "Anonymous";
// }
messagingStyle.addMessage(comment, post.time, name);
}
}
@TargetApi(Build.VERSION_CODES.O)
private void setNotificationIntent(NotificationCompat.Builder builder) {
Intent intent = new Intent(applicationContext, BoardActivity.class);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_SINGLE_TOP |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
PendingIntent pendingIntent = PendingIntent.getActivity(
applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
}
@TargetApi(Build.VERSION_CODES.O)
private void ensureChannels() {
NotificationChannel normalChannel = new NotificationChannel(
CHANNEL_ID_WATCH_NORMAL, "Thread updates",
NotificationManager.IMPORTANCE_DEFAULT);
normalChannel.setDescription("Normal posts for threads");
notificationManager.createNotificationChannel(normalChannel);
NotificationChannel mentionChannel = new NotificationChannel(
CHANNEL_ID_WATCH_MENTION, "Thread mentions",
NotificationManager.IMPORTANCE_HIGH);
mentionChannel.setDescription("Posts were you were mentioned");
mentionChannel.enableVibration(true);
mentionChannel.enableLights(true);
notificationManager.createNotificationChannel(mentionChannel);
}
}

@ -24,6 +24,7 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
@ -69,8 +70,6 @@ public class WatchNotifier extends Service {
inject(this);
nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
startForeground(NOTIFICATION_ID, createNotification());
}
@Override
@ -91,13 +90,17 @@ public class WatchNotifier extends Service {
}
public void updateNotification() {
nm.notify(NOTIFICATION_ID, createNotification());
Notification notification = createNotification();
if (notification != null) {
nm.notify(NOTIFICATION_ID, notification);
}
}
public void pausePins() {
watchManager.pauseAll();
}
@Nullable
private Notification createNotification() {
boolean notifyQuotesOnly = ChanSettings.watchNotifyMode.get().equals("quotes");
boolean soundQuotesOnly = ChanSettings.watchSound.get().equals("quotes");
@ -162,15 +165,12 @@ public class WatchNotifier extends Service {
return notifyAboutPosts(pins, subjectPins, unviewedPosts, listQuoting, notifyQuotesOnly, light, sound, peek);
}
@Nullable
private Notification notifyAboutPosts(List<Pin> pins, List<Pin> subjectPins, List<Post> unviewedPosts, List<Post> listQuoting,
boolean notifyQuotesOnly, boolean light, boolean sound, boolean peek) {
String title = getResources().getQuantityString(R.plurals.watch_title, pins.size(), pins.size());
if (unviewedPosts.isEmpty()) {
// Idle notification
String message = getString(R.string.watch_idle);
return get(title, message, null, false, false, false, false, null);
} else {
return null;
}
// New posts notification
String message;
List<Post> postsForExpandedLines;
@ -215,7 +215,6 @@ public class WatchNotifier extends Service {
String smallText = TextUtils.join(", ", expandedLines);
return get(message, smallText, expandedLines, light, sound, peek, true, targetPin);
}
}
/**
* Create a notification with the supplied parameters.

Loading…
Cancel
Save