diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 98ae946966137f0e12734fbafe8b3c0305c2479f..dc399cd7e12823ff4435c1278e3e4ac4b10bede0 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -134,8 +134,10 @@ <string name="introduction_response_accepted_received">%1$s accepted the introduction to %2$s.</string> <string name="introduction_response_declined_received">%1$s declined the introduction to %2$s.</string> <string name="introduction_response_declined_received_by_introducee">%1$s says that %2$s declined the introduction.</string> - <string name="introduction_success_title">Introduced contact was added</string> - <string name="introduction_success_text">You have been introduced to %1$s.</string> + <plurals name="introduction_notification_text"> + <item quantity="one">New contact added.</item> + <item quantity="other">%d new contacts added.</item> + </plurals> <!-- Forums --> <string name="no_forums">You don\'t have any forums yet.\n\nWhy don\'t you create a new one yourself by tapping the + icon at the top?\n\nYou can also ask your contacts to share forums with you.</string> diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java index dc53745b9839a995bcf16621ec4cca54f382b10a..966de01db949176bda374f03248e51174c1c0b88 100644 --- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java +++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java @@ -2,10 +2,14 @@ package org.briarproject.android; import android.app.Application; import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; import android.os.Build; +import android.support.annotation.UiThread; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; @@ -14,7 +18,6 @@ import org.briarproject.android.api.AndroidExecutor; import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.contact.ConversationActivity; import org.briarproject.android.forum.ForumActivity; -import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.db.DbException; @@ -59,20 +62,38 @@ import static android.support.v4.app.NotificationCompat.CATEGORY_SOCIAL; import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET; import static java.util.logging.Level.WARNING; import static org.briarproject.android.BriarActivity.GROUP_ID; +import static org.briarproject.android.NavDrawerActivity.INTENT_BLOGS; +import static org.briarproject.android.NavDrawerActivity.INTENT_CONTACTS; +import static org.briarproject.android.NavDrawerActivity.INTENT_FORUMS; import static org.briarproject.android.fragment.SettingsFragment.PREF_NOTIFY_BLOG; import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE; class AndroidNotificationManagerImpl implements AndroidNotificationManager, Service, EventListener { + // Notification IDs private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3; private static final int FORUM_POST_NOTIFICATION_ID = 4; private static final int BLOG_POST_NOTIFICATION_ID = 5; private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 6; + + // Content URIs to differentiate between pending intents private static final String CONTACT_URI = "content://org.briarproject/contact"; private static final String FORUM_URI = "content://org.briarproject/forum"; + private static final String BLOG_URI = + "content://org.briarproject/blog"; + + // Actions for intents that are broadcast when notifications are dismissed + private static final String CLEAR_PRIVATE_MESSAGE_ACTION = + "org.briarproject.briar.CLEAR_PRIVATE_MESSAGE_NOTIFICATION"; + private static final String CLEAR_FORUM_ACTION = + "org.briarproject.briar.CLEAR_FORUM_NOTIFICATION"; + private static final String CLEAR_BLOG_ACTION = + "org.briarproject.briar.CLEAR_BLOG_NOTIFICATION"; + private static final String CLEAR_INTRODUCTION_ACTION = + "org.briarproject.briar.CLEAR_INTRODUCTION_NOTIFICATION"; private static final Logger LOG = Logger.getLogger(AndroidNotificationManagerImpl.class.getName()); @@ -82,16 +103,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, private final MessagingManager messagingManager; private final AndroidExecutor androidExecutor; private final Context appContext; + private final BroadcastReceiver receiver = new DeleteIntentReceiver(); + private final AtomicBoolean used = new AtomicBoolean(false); // The following must only be accessed on the main UI thread private final Map<GroupId, Integer> contactCounts = new HashMap<>(); private final Map<GroupId, Integer> forumCounts = new HashMap<>(); - private final AtomicBoolean used = new AtomicBoolean(false); - + private final Map<GroupId, Integer> blogCounts = new HashMap<>(); private int contactTotal = 0, forumTotal = 0, blogTotal = 0; + private int introductionTotal = 0; private int nextRequestId = 0; - private GroupId visibleGroup = null; - private boolean blogBlocked = false; + private GroupId blockedGroup = null; + private boolean blockContacts = false, blockForums = false; + private boolean blockBlogs = false, blockIntroductions = false; private volatile Settings settings = new Settings(); @@ -109,21 +133,43 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, @Override public void startService() throws ServiceException { if (used.getAndSet(true)) throw new IllegalStateException(); + // Load settings try { settings = settingsManager.getSettings(SETTINGS_NAMESPACE); } catch (DbException e) { throw new ServiceException(e); } + // Register a broadcast receiver for notifications being dismissed + Future<Void> f = androidExecutor.runOnUiThread(new Callable<Void>() { + @Override + public Void call() { + IntentFilter filter = new IntentFilter(); + filter.addAction(CLEAR_PRIVATE_MESSAGE_ACTION); + filter.addAction(CLEAR_FORUM_ACTION); + filter.addAction(CLEAR_BLOG_ACTION); + filter.addAction(CLEAR_INTRODUCTION_ACTION); + appContext.registerReceiver(receiver, filter); + return null; + } + }); + try { + f.get(); + } catch (InterruptedException | ExecutionException e) { + throw new ServiceException(e); + } } @Override public void stopService() throws ServiceException { - Future<Void> f = androidExecutor.submit(new Callable<Void>() { + // Clear all notifications and unregister the broadcast receiver + Future<Void> f = androidExecutor.runOnUiThread(new Callable<Void>() { @Override public Void call() { clearPrivateMessageNotification(); clearForumPostNotification(); + clearBlogPostNotification(); clearIntroductionSuccessNotification(); + appContext.unregisterReceiver(receiver); return null; } }); @@ -134,19 +180,36 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } } + @UiThread private void clearPrivateMessageNotification() { + contactCounts.clear(); + contactTotal = 0; Object o = appContext.getSystemService(NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) o; nm.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID); } + @UiThread private void clearForumPostNotification() { + forumCounts.clear(); + forumTotal = 0; Object o = appContext.getSystemService(NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) o; nm.cancel(FORUM_POST_NOTIFICATION_ID); } + @UiThread + private void clearBlogPostNotification() { + blogCounts.clear(); + blogTotal = 0; + Object o = appContext.getSystemService(NOTIFICATION_SERVICE); + NotificationManager nm = (NotificationManager) o; + nm.cancel(BLOG_POST_NOTIFICATION_ID); + } + + @UiThread private void clearIntroductionSuccessNotification() { + introductionTotal = 0; Object o = appContext.getSystemService(NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) o; nm.cancel(INTRODUCTION_SUCCESS_NOTIFICATION_ID); @@ -158,29 +221,28 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, SettingsUpdatedEvent s = (SettingsUpdatedEvent) e; if (s.getNamespace().equals(SETTINGS_NAMESPACE)) loadSettings(); } else if (e instanceof PrivateMessageReceivedEvent) { - PrivateMessageReceivedEvent m = (PrivateMessageReceivedEvent) e; - showPrivateMessageNotification(m.getGroupId()); + PrivateMessageReceivedEvent p = (PrivateMessageReceivedEvent) e; + showPrivateMessageNotification(p.getGroupId()); } else if (e instanceof ForumPostReceivedEvent) { - ForumPostReceivedEvent m = (ForumPostReceivedEvent) e; - showForumPostNotification(m.getGroupId()); + ForumPostReceivedEvent f = (ForumPostReceivedEvent) e; + showForumPostNotification(f.getGroupId()); } else if (e instanceof BlogPostAddedEvent) { - BlogPostAddedEvent be = (BlogPostAddedEvent) e; - showBlogPostNotification(be.getGroupId()); + BlogPostAddedEvent b = (BlogPostAddedEvent) e; + showBlogPostNotification(b.getGroupId()); } else if (e instanceof IntroductionRequestReceivedEvent) { ContactId c = ((IntroductionRequestReceivedEvent) e).getContactId(); showNotificationForPrivateConversation(c); } else if (e instanceof IntroductionResponseReceivedEvent) { ContactId c = ((IntroductionResponseReceivedEvent) e).getContactId(); showNotificationForPrivateConversation(c); - } else if (e instanceof IntroductionSucceededEvent) { - Contact c = ((IntroductionSucceededEvent) e).getContact(); - showIntroductionSucceededNotification(c); } else if (e instanceof InvitationReceivedEvent) { ContactId c = ((InvitationReceivedEvent) e).getContactId(); showNotificationForPrivateConversation(c); } else if (e instanceof InvitationResponseReceivedEvent) { ContactId c = ((InvitationResponseReceivedEvent) e).getContactId(); showNotificationForPrivateConversation(c); + } else if (e instanceof IntroductionSucceededEvent) { + showIntroductionNotification(); } } @@ -198,35 +260,35 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, }); } - @Override - public void showPrivateMessageNotification(final GroupId g) { - androidExecutor.execute(new Runnable() { + private void showPrivateMessageNotification(final GroupId g) { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { + if (blockContacts) return; + if (g.equals(blockedGroup)) return; Integer count = contactCounts.get(g); if (count == null) contactCounts.put(g, 1); else contactCounts.put(g, count + 1); contactTotal++; - if (!g.equals(visibleGroup)) - updatePrivateMessageNotification(); + updatePrivateMessageNotification(); } }); } @Override public void clearPrivateMessageNotification(final GroupId g) { - androidExecutor.execute(new Runnable() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { Integer count = contactCounts.remove(g); if (count == null) return; // Already cleared contactTotal -= count; - // FIXME: If the notification isn't showing, this may show it updatePrivateMessageNotification(); } }); } + @UiThread private void updatePrivateMessageNotification() { if (contactTotal == 0) { clearPrivateMessageNotification(); @@ -245,7 +307,13 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, b.setDefaults(getDefaults()); b.setOnlyAlertOnce(true); b.setAutoCancel(true); + // Clear the counters if the notification is dismissed + Intent clear = new Intent(CLEAR_PRIVATE_MESSAGE_ACTION); + PendingIntent delete = PendingIntent.getBroadcast(appContext, 0, + clear, 0); + b.setDeleteIntent(delete); if (contactCounts.size() == 1) { + // Touching the notification shows the relevant conversation Intent i = new Intent(appContext, ConversationActivity.class); GroupId g = contactCounts.keySet().iterator().next(); i.putExtra(GROUP_ID, g.getBytes()); @@ -257,9 +325,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, t.addNextIntent(i); b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); } else { + // Touching the notification shows the contact list Intent i = new Intent(appContext, NavDrawerActivity.class); - i.putExtra(NavDrawerActivity.INTENT_CONTACTS, true); + i.putExtra(INTENT_CONTACTS, true); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); + i.setData(Uri.parse(CONTACT_URI)); TaskStackBuilder t = TaskStackBuilder.create(appContext); t.addParentStack(NavDrawerActivity.class); t.addNextIntent(i); @@ -275,6 +345,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } } + @UiThread private int getDefaults() { int defaults = DEFAULT_LIGHTS; boolean sound = settings.getBoolean("notifySound", true); @@ -287,34 +358,46 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } @Override - public void showForumPostNotification(final GroupId g) { - androidExecutor.execute(new Runnable() { + public void clearAllContactNotifications() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { + clearPrivateMessageNotification(); + clearIntroductionSuccessNotification(); + } + }); + } + + @UiThread + private void showForumPostNotification(final GroupId g) { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + if (blockForums) return; + if (g.equals(blockedGroup)) return; Integer count = forumCounts.get(g); if (count == null) forumCounts.put(g, 1); else forumCounts.put(g, count + 1); forumTotal++; - if (!g.equals(visibleGroup)) - updateForumPostNotification(); + updateForumPostNotification(); } }); } @Override public void clearForumPostNotification(final GroupId g) { - androidExecutor.execute(new Runnable() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { Integer count = forumCounts.remove(g); if (count == null) return; // Already cleared forumTotal -= count; - // FIXME: If the notification isn't showing, this may show it updateForumPostNotification(); } }); } + @UiThread private void updateForumPostNotification() { if (forumTotal == 0) { clearForumPostNotification(); @@ -332,7 +415,13 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, b.setDefaults(getDefaults()); b.setOnlyAlertOnce(true); b.setAutoCancel(true); + // Clear the counters if the notification is dismissed + Intent clear = new Intent(CLEAR_FORUM_ACTION); + PendingIntent delete = PendingIntent.getBroadcast(appContext, 0, + clear, 0); + b.setDeleteIntent(delete); if (forumCounts.size() == 1) { + // Touching the notification shows the relevant forum Intent i = new Intent(appContext, ForumActivity.class); GroupId g = forumCounts.keySet().iterator().next(); i.putExtra(GROUP_ID, g.getBytes()); @@ -344,9 +433,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, t.addNextIntent(i); b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); } else { + // Touching the notification shows the forum list Intent i = new Intent(appContext, NavDrawerActivity.class); - i.putExtra(NavDrawerActivity.INTENT_FORUMS, true); + i.putExtra(INTENT_FORUMS, true); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); + i.setData(Uri.parse(FORUM_URI)); TaskStackBuilder t = TaskStackBuilder.create(appContext); t.addParentStack(NavDrawerActivity.class); t.addNextIntent(i); @@ -363,28 +454,49 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } @Override - public void showBlogPostNotification(final GroupId g) { - androidExecutor.execute(new Runnable() { + public void clearAllForumPostNotifications() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { - if (!blogBlocked) { - blogTotal++; - updateBlogPostNotification(); - } + clearForumPostNotification(); + } + }); + } + + @UiThread + private void showBlogPostNotification(final GroupId g) { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + if (blockBlogs) return; + if (g.equals(blockedGroup)) return; + Integer count = blogCounts.get(g); + if (count == null) blogCounts.put(g, 1); + else blogCounts.put(g, count + 1); + blogTotal++; + updateBlogPostNotification(); } }); } @Override - public void clearBlogPostNotification() { - blogTotal = 0; - Object o = appContext.getSystemService(NOTIFICATION_SERVICE); - NotificationManager nm = (NotificationManager) o; - nm.cancel(BLOG_POST_NOTIFICATION_ID); + public void clearBlogPostNotification(final GroupId g) { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + Integer count = blogCounts.remove(g); + if (count == null) return; // Already cleared + blogTotal -= count; + updateBlogPostNotification(); + } + }); } + @UiThread private void updateBlogPostNotification() { - if (settings.getBoolean(PREF_NOTIFY_BLOG, true)) { + if (blogTotal == 0) { + clearBlogPostNotification(); + } else if (settings.getBoolean(PREF_NOTIFY_BLOG, true)) { NotificationCompat.Builder b = new NotificationCompat.Builder(appContext); b.setSmallIcon(R.drawable.message_notification_icon); @@ -398,15 +510,20 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, b.setDefaults(getDefaults()); b.setOnlyAlertOnce(true); b.setAutoCancel(true); - + // Clear the counters if the notification is dismissed + Intent clear = new Intent(CLEAR_BLOG_ACTION); + PendingIntent delete = PendingIntent.getBroadcast(appContext, 0, + clear, 0); + b.setDeleteIntent(delete); + // Touching the notification shows the combined blog feed Intent i = new Intent(appContext, NavDrawerActivity.class); - i.putExtra(NavDrawerActivity.INTENT_BLOGS, true); + i.putExtra(INTENT_BLOGS, true); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); + i.setData(Uri.parse(BLOG_URI)); TaskStackBuilder t = TaskStackBuilder.create(appContext); t.addParentStack(NavDrawerActivity.class); t.addNextIntent(i); b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); - if (Build.VERSION.SDK_INT >= 21) { b.setCategory(CATEGORY_SOCIAL); b.setVisibility(VISIBILITY_SECRET); @@ -417,90 +534,181 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } } + @Override + public void clearAllBlogPostNotifications() { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + clearBlogPostNotification(); + } + }); + } + + private void showIntroductionNotification() { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + if (blockIntroductions) return; + introductionTotal++; + updateIntroductionNotification(); + } + }); + } + + @UiThread + private void updateIntroductionNotification() { + NotificationCompat.Builder b = + new NotificationCompat.Builder(appContext); + b.setSmallIcon(R.drawable.introduction_notification); + b.setContentTitle(appContext.getText(R.string.app_name)); + b.setContentText(appContext.getResources().getQuantityString( + R.plurals.introduction_notification_text, introductionTotal, + introductionTotal)); + String ringtoneUri = settings.get("notifyRingtoneUri"); + if (!StringUtils.isNullOrEmpty(ringtoneUri)) + b.setSound(Uri.parse(ringtoneUri)); + b.setDefaults(getDefaults()); + b.setOnlyAlertOnce(true); + b.setAutoCancel(true); + // Clear the counter if the notification is dismissed + Intent clear = new Intent(CLEAR_INTRODUCTION_ACTION); + PendingIntent delete = PendingIntent.getBroadcast(appContext, 0, + clear, 0); + b.setDeleteIntent(delete); + // Touching the notification shows the contact list + Intent i = new Intent(appContext, NavDrawerActivity.class); + i.putExtra(INTENT_CONTACTS, true); + i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); + i.setData(Uri.parse(CONTACT_URI)); + TaskStackBuilder t = TaskStackBuilder.create(appContext); + t.addParentStack(NavDrawerActivity.class); + t.addNextIntent(i); + b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); + if (Build.VERSION.SDK_INT >= 21) { + b.setCategory(CATEGORY_MESSAGE); + b.setVisibility(VISIBILITY_SECRET); + } + Object o = appContext.getSystemService(NOTIFICATION_SERVICE); + NotificationManager nm = (NotificationManager) o; + nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build()); + } + @Override public void blockNotification(final GroupId g) { - androidExecutor.execute(new Runnable() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { - visibleGroup = g; + blockedGroup = g; } }); } @Override public void unblockNotification(final GroupId g) { - androidExecutor.execute(new Runnable() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { - if (g.equals(visibleGroup)) visibleGroup = null; + if (g.equals(blockedGroup)) blockedGroup = null; } }); } @Override - public void blockBlogNotification() { - androidExecutor.execute(new Runnable() { + public void blockAllContactNotifications() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { - blogBlocked = true; + blockContacts = true; + blockIntroductions = true; } }); } @Override - public void unblockBlogNotification() { - androidExecutor.execute(new Runnable() { + public void unblockAllContactNotifications() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { - blogBlocked = false; + blockContacts = false; + blockIntroductions = false; } }); } - private void showNotificationForPrivateConversation(final ContactId c) { - androidExecutor.execute(new Runnable() { + @Override + public void blockAllForumPostNotifications() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { - try { - GroupId group = messagingManager.getConversationId(c); - showPrivateMessageNotification(group); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } + blockForums = true; } }); } - private void showIntroductionSucceededNotification(final Contact c) { - androidExecutor.execute(new Runnable() { + @Override + public void unblockAllForumPostNotifications() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { - NotificationCompat.Builder b = - new NotificationCompat.Builder(appContext); - b.setSmallIcon(R.drawable.introduction_notification); - - b.setContentTitle(appContext - .getString(R.string.introduction_success_title)); - b.setContentText(appContext - .getString(R.string.introduction_success_text, - c.getAuthor().getName())); - b.setDefaults(getDefaults()); - b.setAutoCancel(true); + blockForums = false; + } + }); + } - Intent i = new Intent(appContext, NavDrawerActivity.class); - i.putExtra(NavDrawerActivity.INTENT_CONTACTS, true); - i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); - TaskStackBuilder t = TaskStackBuilder.create(appContext); - t.addParentStack(NavDrawerActivity.class); - t.addNextIntent(i); - b.setContentIntent(t.getPendingIntent(nextRequestId++, 0)); + @Override + public void blockAllBlogPostNotifications() { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + blockBlogs = true; + } + }); + } + + @Override + public void unblockAllBlogPostNotifications() { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + blockBlogs = false; + } + }); + } - Object o = appContext.getSystemService(NOTIFICATION_SERVICE); - NotificationManager nm = (NotificationManager) o; - nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build()); + private void showNotificationForPrivateConversation(final ContactId c) { + dbExecutor.execute(new Runnable() { + @Override + public void run() { + try { + GroupId g = messagingManager.getConversationId(c); + showPrivateMessageNotification(g); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } } }); } + private class DeleteIntentReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + if (CLEAR_PRIVATE_MESSAGE_ACTION.equals(action)) { + clearPrivateMessageNotification(); + } else if (CLEAR_FORUM_ACTION.equals(action)) { + clearForumPostNotification(); + } else if (CLEAR_BLOG_ACTION.equals(action)) { + clearBlogPostNotification(); + } else if (CLEAR_INTRODUCTION_ACTION.equals(action)) { + clearIntroductionSuccessNotification(); + } + } + }); + } + } } diff --git a/briar-android/src/org/briarproject/android/BriarService.java b/briar-android/src/org/briarproject/android/BriarService.java index 33d69c8f68e2a6ed7144d4e69014b51b7f783531..f03751821ddcc5886cb31715e32ffc9f1896140e 100644 --- a/briar-android/src/org/briarproject/android/BriarService.java +++ b/briar-android/src/org/briarproject/android/BriarService.java @@ -108,7 +108,8 @@ public class BriarService extends Service { } private void showStartupFailureNotification(final StartResult result) { - androidExecutor.execute(new Runnable() { + androidExecutor.runOnUiThread(new Runnable() { + @Override public void run() { NotificationCompat.Builder b = new NotificationCompat.Builder(BriarService.this); @@ -197,11 +198,13 @@ public class BriarService extends Service { private volatile IBinder binder = null; + @Override public void onServiceConnected(ComponentName name, IBinder binder) { this.binder = binder; binderLatch.countDown(); } + @Override public void onServiceDisconnected(ComponentName name) {} /** Waits for the service to connect and returns its binder. */ diff --git a/briar-android/src/org/briarproject/android/SplashScreenActivity.java b/briar-android/src/org/briarproject/android/SplashScreenActivity.java index 0933d35c3e06d8e217c2ac27e51f7582c6f04b1b..2ac2b5bf2f5734b779cbc791ec9360855fafebc8 100644 --- a/briar-android/src/org/briarproject/android/SplashScreenActivity.java +++ b/briar-android/src/org/briarproject/android/SplashScreenActivity.java @@ -87,7 +87,7 @@ public class SplashScreenActivity extends BaseActivity { } private void setPreferencesDefaults() { - androidExecutor.execute(new Runnable() { + androidExecutor.runOnBackgroundThread(new Runnable() { @Override public void run() { PreferenceManager.setDefaultValues(SplashScreenActivity.this, diff --git a/briar-android/src/org/briarproject/android/api/AndroidExecutor.java b/briar-android/src/org/briarproject/android/api/AndroidExecutor.java index 047a74c14701e85a174f0acc9e78a16038aab638..7ea06bc2bd020841367da67fc2cb27b73367a911 100644 --- a/briar-android/src/org/briarproject/android/api/AndroidExecutor.java +++ b/briar-android/src/org/briarproject/android/api/AndroidExecutor.java @@ -13,10 +13,21 @@ public interface AndroidExecutor { * Runs the given task on a background thread with a message queue and * returns a Future for getting the result. */ - <V> Future<V> submit(Callable<V> c); + <V> Future<V> runOnBackgroundThread(Callable<V> c); /** * Runs the given task on a background thread with a message queue. */ - void execute(Runnable r); + void runOnBackgroundThread(Runnable r); + + /** + * Runs the given task on the main UI thread and returns a Future for + * getting the result. + */ + <V> Future<V> runOnUiThread(Callable<V> c); + + /** + * Runs the given task on the main UI thread. + */ + void runOnUiThread(Runnable r); } diff --git a/briar-android/src/org/briarproject/android/api/AndroidNotificationManager.java b/briar-android/src/org/briarproject/android/api/AndroidNotificationManager.java index 776ed723af1def7b3148a47144548231c3bd0512..217d8937d6a51c73398ddd7fa82bbd43801aa69b 100644 --- a/briar-android/src/org/briarproject/android/api/AndroidNotificationManager.java +++ b/briar-android/src/org/briarproject/android/api/AndroidNotificationManager.java @@ -2,26 +2,37 @@ package org.briarproject.android.api; import org.briarproject.api.sync.GroupId; -/** Manages notifications for private messages and forum posts. */ +/** + * Manages notifications for private messages, forum posts, blog posts and + * introductions. + */ public interface AndroidNotificationManager { - void showPrivateMessageNotification(GroupId g); - void clearPrivateMessageNotification(GroupId g); - void showForumPostNotification(GroupId g); + void clearAllContactNotifications(); void clearForumPostNotification(GroupId g); - void showBlogPostNotification(GroupId g); + void clearAllForumPostNotifications(); + + void clearBlogPostNotification(GroupId g); - void clearBlogPostNotification(); + void clearAllBlogPostNotifications(); void blockNotification(GroupId g); void unblockNotification(GroupId g); - void blockBlogNotification(); + void blockAllContactNotifications(); + + void unblockAllContactNotifications(); + + void blockAllForumPostNotifications(); + + void unblockAllForumPostNotifications(); + + void blockAllBlogPostNotifications(); - void unblockBlogNotification(); + void unblockAllBlogPostNotifications(); } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java index fbed89b0c9943656fa845cb1856c410dde1d35c9..15552fdb1ac80bcba5149fa25509a24b76c86212 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java @@ -2,6 +2,7 @@ package org.briarproject.android.blogs; import android.app.Activity; +import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.controller.DbControllerImpl; import org.briarproject.android.controller.handler.ResultExceptionHandler; import org.briarproject.api.blogs.Blog; @@ -38,6 +39,8 @@ public class BlogControllerImpl extends DbControllerImpl protected Activity activity; @Inject protected EventBus eventBus; + @Inject + protected AndroidNotificationManager notificationManager; @Inject protected volatile BlogManager blogManager; @@ -71,11 +74,14 @@ public class BlogControllerImpl extends DbControllerImpl @Override public void onActivityResume() { + notificationManager.blockNotification(groupId); + notificationManager.clearBlogPostNotification(groupId); eventBus.addListener(this); } @Override public void onActivityPause() { + notificationManager.unblockNotification(groupId); eventBus.removeListener(this); } diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java index 143fc1b10d0a3dca946783c4a0ccb7c6ed204c79..0e89542af0459ebd9e35411353adf62a39edde18 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java @@ -122,6 +122,7 @@ public class BlogFragment extends BaseFragment implements BlogPostListener { @Override public void injectFragment(ActivityComponent component) { component.inject(this); + blogController.setGroupId(groupId); } @Override diff --git a/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java index e02022ac1c238e7b0ea7572d0f614c0e28b8ad56..7c093598b156da61c6d04d6bca7758f7c1b88161 100644 --- a/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java +++ b/briar-android/src/org/briarproject/android/blogs/FeedControllerImpl.java @@ -33,12 +33,13 @@ public class FeedControllerImpl extends DbControllerImpl @SuppressWarnings("WeakerAccess") @Inject AndroidNotificationManager notificationManager; + @Inject + protected EventBus eventBus; + @Inject protected volatile BlogManager blogManager; @Inject protected volatile IdentityManager identityManager; - @Inject - protected volatile EventBus eventBus; private volatile OnBlogPostAddedListener listener; @@ -48,14 +49,14 @@ public class FeedControllerImpl extends DbControllerImpl @Override public void onResume() { - notificationManager.blockBlogNotification(); - notificationManager.clearBlogPostNotification(); + notificationManager.blockAllBlogPostNotifications(); + notificationManager.clearAllBlogPostNotifications(); eventBus.addListener(this); } @Override public void onPause() { - notificationManager.unblockBlogNotification(); + notificationManager.unblockAllBlogPostNotifications(); eventBus.removeListener(this); } diff --git a/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java b/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java index 7b5e2caa9c74a3e9bf8d970b22260423a52fc552..4e6d24312c74964e1b68a4004bd5843a2c82c9da 100644 --- a/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java +++ b/briar-android/src/org/briarproject/android/blogs/WriteBlogPostActivity.java @@ -18,6 +18,7 @@ import android.widget.TextView.OnEditorActionListener; import org.briarproject.R; import org.briarproject.android.ActivityComponent; import org.briarproject.android.BriarActivity; +import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.api.FormatException; import org.briarproject.api.blogs.BlogManager; import org.briarproject.api.blogs.BlogPost; @@ -47,6 +48,9 @@ public class WriteBlogPostActivity extends BriarActivity Logger.getLogger(WriteBlogPostActivity.class.getName()); private static final String contentType = "text/plain"; + @Inject + protected AndroidNotificationManager notificationManager; + private TextInputEditText titleInput; private EditText bodyInput; private Button publishButton; @@ -69,13 +73,8 @@ public class WriteBlogPostActivity extends BriarActivity byte[] b = i.getByteArrayExtra(GROUP_ID); if (b == null) throw new IllegalStateException("No Group in intent."); groupId = new GroupId(b); -// String blogName = i.getStringExtra(BLOG_NAME); -// if (blogName != null) setTitle(blogName); setContentView(R.layout.activity_write_blog_post); -// String title = -// getTitle() + ": " + getString(R.string.blogs_write_blog_post); -// setTitle(title); TextInputLayout titleLayout = (TextInputLayout) findViewById(R.id.titleLayout); @@ -116,6 +115,18 @@ public class WriteBlogPostActivity extends BriarActivity progressBar = (ProgressBar) findViewById(R.id.progressBar); } + @Override + public void onPause() { + super.onPause(); + notificationManager.unblockNotification(groupId); + } + + @Override + public void onResume() { + super.onResume(); + notificationManager.blockNotification(groupId); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java index afda3f0cf9ee6d78309337dbe9f57e9ac99fa5f5..bead9c4db6c61e226db713f546155dd44e7122c0 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java @@ -17,6 +17,7 @@ import android.view.ViewGroup; import org.briarproject.R; import org.briarproject.android.ActivityComponent; +import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.fragment.BaseFragment; import org.briarproject.android.keyagreement.KeyAgreementActivity; import org.briarproject.android.util.BriarRecyclerView; @@ -75,6 +76,8 @@ public class ContactListFragment extends BaseFragment implements EventListener { ConnectionRegistry connectionRegistry; @Inject protected EventBus eventBus; + @Inject + protected AndroidNotificationManager notificationManager; private ContactListAdapter adapter = null; private BriarRecyclerView list = null; @@ -184,6 +187,8 @@ public class ContactListFragment extends BaseFragment implements EventListener { @Override public void onResume() { super.onResume(); + notificationManager.blockAllContactNotifications(); + notificationManager.clearAllContactNotifications(); eventBus.addListener(this); loadContacts(); list.startPeriodicUpdate(); @@ -193,6 +198,7 @@ public class ContactListFragment extends BaseFragment implements EventListener { public void onPause() { super.onPause(); eventBus.removeListener(this); + notificationManager.unblockAllContactNotifications(); adapter.clear(); list.showProgressBar(); list.stopPeriodicUpdate(); diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java index 0f33788523b735f03558330e6f012f0531df0aed..11c9bd293099b9dc9c4e9b429055c03d0a5b4ea4 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java +++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java @@ -15,6 +15,7 @@ import android.view.ViewGroup; import org.briarproject.R; import org.briarproject.android.ActivityComponent; +import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.fragment.BaseEventFragment; import org.briarproject.android.sharing.InvitationsForumActivity; import org.briarproject.android.util.BriarRecyclerView; @@ -55,9 +56,14 @@ public class ForumListFragment extends BaseEventFragment implements private ForumListAdapter adapter; private Snackbar snackbar; + @Inject + protected AndroidNotificationManager notificationManager; + // Fields that are accessed from background threads must be volatile - @Inject protected volatile ForumManager forumManager; - @Inject protected volatile ForumSharingManager forumSharingManager; + @Inject + protected volatile ForumManager forumManager; + @Inject + protected volatile ForumSharingManager forumSharingManager; public static ForumListFragment newInstance() { @@ -109,6 +115,8 @@ public class ForumListFragment extends BaseEventFragment implements public void onResume() { super.onResume(); + notificationManager.blockAllForumPostNotifications(); + notificationManager.clearAllForumPostNotifications(); loadForumHeaders(); loadAvailableForums(); list.startPeriodicUpdate(); @@ -118,6 +126,7 @@ public class ForumListFragment extends BaseEventFragment implements public void onPause() { super.onPause(); + notificationManager.unblockAllForumPostNotifications(); adapter.clear(); list.showProgressBar(); list.stopPeriodicUpdate(); diff --git a/briar-android/src/org/briarproject/android/fragment/SettingsFragment.java b/briar-android/src/org/briarproject/android/fragment/SettingsFragment.java index 7bde67e6e224a2051ffd83421c5ee96ea99a150b..5b768aaa1a1370b7b83c7bc5f9ce67a2af229ec7 100644 --- a/briar-android/src/org/briarproject/android/fragment/SettingsFragment.java +++ b/briar-android/src/org/briarproject/android/fragment/SettingsFragment.java @@ -173,6 +173,7 @@ public class SettingsFragment extends PreferenceFragmentCompat private void loadSettings() { listener.runOnDbThread(new Runnable() { + @Override public void run() { try { long now = System.currentTimeMillis(); @@ -195,6 +196,7 @@ public class SettingsFragment extends PreferenceFragmentCompat private void displaySettings() { listener.runOnUiThread(new Runnable() { + @Override public void run() { enableBluetooth.setValue(Boolean.toString(bluetoothSetting)); torOverMobile.setValue(Boolean.toString(torSetting)); @@ -228,7 +230,8 @@ public class SettingsFragment extends PreferenceFragmentCompat } private void triggerFeedback() { - androidExecutor.execute(new Runnable() { + androidExecutor.runOnBackgroundThread(new Runnable() { + @Override public void run() { ACRA.getErrorReporter().handleException(new UserFeedback(), false); @@ -268,7 +271,8 @@ public class SettingsFragment extends PreferenceFragmentCompat private void enableOrDisableBluetooth(final boolean enable) { final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter != null) { - androidExecutor.execute(new Runnable() { + androidExecutor.runOnBackgroundThread(new Runnable() { + @Override public void run() { if (enable) adapter.enable(); else adapter.disable(); @@ -279,6 +283,7 @@ public class SettingsFragment extends PreferenceFragmentCompat private void storeTorSettings() { listener.runOnDbThread(new Runnable() { + @Override public void run() { try { Settings s = new Settings(); @@ -298,6 +303,7 @@ public class SettingsFragment extends PreferenceFragmentCompat private void storeBluetoothSettings() { listener.runOnDbThread(new Runnable() { + @Override public void run() { try { Settings s = new Settings(); @@ -317,6 +323,7 @@ public class SettingsFragment extends PreferenceFragmentCompat private void storeSettings(final Settings settings) { listener.runOnDbThread(new Runnable() { + @Override public void run() { try { long now = System.currentTimeMillis(); diff --git a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java index 2ea18075dc29ee444884047e1914b2f16c4181b3..82fca3e36d63c587eac49ee6e7edabe278b00437 100644 --- a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java +++ b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java @@ -144,7 +144,7 @@ public class ShowQrCodeFragment extends BaseEventFragment final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter != null && !adapter.isEnabled()) { waitingForBluetooth = true; - androidExecutor.execute(new Runnable() { + androidExecutor.runOnBackgroundThread(new Runnable() { @Override public void run() { adapter.enable(); diff --git a/briar-android/src/org/briarproject/android/panic/PanicResponderActivity.java b/briar-android/src/org/briarproject/android/panic/PanicResponderActivity.java index ff3ba954a7f0580b71cb196a020ce368f383287d..ba8583fcac04e9cf99c11a27569251e42ad1a5a4 100644 --- a/briar-android/src/org/briarproject/android/panic/PanicResponderActivity.java +++ b/briar-android/src/org/briarproject/android/panic/PanicResponderActivity.java @@ -107,7 +107,7 @@ public class PanicResponderActivity extends BriarActivity { } private void deleteAllData() { - androidExecutor.execute(new Runnable() { + androidExecutor.runOnBackgroundThread(new Runnable() { @Override public void run() { configController.deleteAccount(PanicResponderActivity.this); diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java index b6f82ebf5ae6a75a2c36487bbae583cff65905a0..686d15b615411ed05858061fcb46f9944c98ba68 100644 --- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java +++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java @@ -126,12 +126,13 @@ class DroidtoothPlugin implements DuplexPlugin { // BluetoothAdapter.getDefaultAdapter() must be called on a thread // with a message queue, so submit it to the AndroidExecutor try { - adapter = androidExecutor.submit(new Callable<BluetoothAdapter>() { - @Override - public BluetoothAdapter call() throws Exception { - return BluetoothAdapter.getDefaultAdapter(); - } - }).get(); + adapter = androidExecutor.runOnBackgroundThread( + new Callable<BluetoothAdapter>() { + @Override + public BluetoothAdapter call() throws Exception { + return BluetoothAdapter.getDefaultAdapter(); + } + }).get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Interrupted while getting BluetoothAdapter"); diff --git a/briar-android/src/org/briarproject/system/AndroidExecutorImpl.java b/briar-android/src/org/briarproject/system/AndroidExecutorImpl.java index 07b6e69e5e89e656e0b61290c551dd1d3b09b3b8..180881febac82ce3d888998f3ac31cbd65bb9893 100644 --- a/briar-android/src/org/briarproject/system/AndroidExecutorImpl.java +++ b/briar-android/src/org/briarproject/system/AndroidExecutorImpl.java @@ -1,5 +1,6 @@ package org.briarproject.system; +import android.app.Application; import android.os.Handler; import android.os.Looper; @@ -16,18 +17,21 @@ import javax.inject.Inject; class AndroidExecutorImpl implements AndroidExecutor { + private final Handler uiHandler; private final Runnable loop; private final AtomicBoolean started = new AtomicBoolean(false); private final CountDownLatch startLatch = new CountDownLatch(1); - private volatile Handler handler = null; + private volatile Handler backgroundHandler = null; @Inject - AndroidExecutorImpl() { + AndroidExecutorImpl(Application app) { + uiHandler = new Handler(app.getApplicationContext().getMainLooper()); loop = new Runnable() { + @Override public void run() { Looper.prepare(); - handler = new Handler(); + backgroundHandler = new Handler(); startLatch.countDown(); Looper.loop(); } @@ -46,14 +50,28 @@ class AndroidExecutorImpl implements AndroidExecutor { } } - public <V> Future<V> submit(Callable<V> c) { + @Override + public <V> Future<V> runOnBackgroundThread(Callable<V> c) { FutureTask<V> f = new FutureTask<>(c); - execute(f); + runOnBackgroundThread(f); return f; } - public void execute(Runnable r) { + @Override + public void runOnBackgroundThread(Runnable r) { startIfNecessary(); - handler.post(r); + backgroundHandler.post(r); + } + + @Override + public <V> Future<V> runOnUiThread(Callable<V> c) { + FutureTask<V> f = new FutureTask<>(c); + runOnUiThread(f); + return f; + } + + @Override + public void runOnUiThread(Runnable r) { + uiHandler.post(r); } } diff --git a/briar-android/src/org/briarproject/system/AndroidSystemModule.java b/briar-android/src/org/briarproject/system/AndroidSystemModule.java index a577ac7c3e1c4d787f80b22d44a406bcb4b170a1..99ca8fb7abf5a18feb793e9dbf1dd3ddeda1929f 100644 --- a/briar-android/src/org/briarproject/system/AndroidSystemModule.java +++ b/briar-android/src/org/briarproject/system/AndroidSystemModule.java @@ -27,7 +27,7 @@ public class AndroidSystemModule { @Provides @Singleton - public AndroidExecutor provideAndroidExecutor() { - return new AndroidExecutorImpl(); + public AndroidExecutor provideAndroidExecutor(Application app) { + return new AndroidExecutorImpl(app); } }