diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 98ae94696..dc399cd7e 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -134,8 +134,10 @@ %1$s accepted the introduction to %2$s. %1$s declined the introduction to %2$s. %1$s says that %2$s declined the introduction. - Introduced contact was added - You have been introduced to %1$s. + + New contact added. + %d new contacts added. + 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. diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java index dc53745b9..966de01db 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 contactCounts = new HashMap<>(); private final Map forumCounts = new HashMap<>(); - private final AtomicBoolean used = new AtomicBoolean(false); - + private final Map 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,22 @@ 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); } - } - - @Override - public void stopService() throws ServiceException { - Future f = androidExecutor.submit(new Callable() { + // Register a broadcast receiver for notifications being dismissed + Future f = androidExecutor.runOnUiThread(new Callable() { @Override public Void call() { - clearPrivateMessageNotification(); - clearForumPostNotification(); - clearIntroductionSuccessNotification(); + 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; } }); @@ -134,19 +159,57 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } } + @Override + public void stopService() throws ServiceException { + // Clear all notifications and unregister the broadcast receiver + Future f = androidExecutor.runOnUiThread(new Callable() { + @Override + public Void call() { + clearPrivateMessageNotification(); + clearForumPostNotification(); + clearBlogPostNotification(); + clearIntroductionSuccessNotification(); + appContext.unregisterReceiver(receiver); + return null; + } + }); + try { + f.get(); + } catch (InterruptedException | ExecutionException e) { + throw new ServiceException(e); + } + } + + @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); @@ -418,52 +535,153 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } @Override - public void blockNotification(final GroupId g) { - androidExecutor.execute(new Runnable() { + public void clearAllBlogPostNotifications() { + androidExecutor.runOnUiThread(new Runnable() { @Override public void run() { - visibleGroup = g; + 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.runOnUiThread(new Runnable() { + @Override + public void run() { + 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; + } + }); + } + + @Override + public void blockAllForumPostNotifications() { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + blockForums = true; + } + }); + } + + @Override + public void unblockAllForumPostNotifications() { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + blockForums = false; + } + }); + } + + @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; } }); } private void showNotificationForPrivateConversation(final ContactId c) { - androidExecutor.execute(new Runnable() { + dbExecutor.execute(new Runnable() { @Override public void run() { try { - GroupId group = messagingManager.getConversationId(c); - showPrivateMessageNotification(group); + GroupId g = messagingManager.getConversationId(c); + showPrivateMessageNotification(g); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -472,35 +690,25 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, }); } - private void showIntroductionSucceededNotification(final Contact c) { - androidExecutor.execute(new Runnable() { - @Override - public void run() { - NotificationCompat.Builder b = - new NotificationCompat.Builder(appContext); - b.setSmallIcon(R.drawable.introduction_notification); + private class DeleteIntentReceiver extends BroadcastReceiver { - 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); - - 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)); - - Object o = appContext.getSystemService(NOTIFICATION_SERVICE); - NotificationManager nm = (NotificationManager) o; - nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build()); - } - }); + @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 33d69c8f6..f03751821 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 0933d35c3..2ac2b5bf2 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 047a74c14..7ea06bc2b 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. */ - Future submit(Callable c); + Future runOnBackgroundThread(Callable 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. + */ + Future runOnUiThread(Callable 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 776ed723a..217d8937d 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(); + void clearBlogPostNotification(GroupId g); + + void clearAllBlogPostNotifications(); void blockNotification(GroupId g); void unblockNotification(GroupId g); - void blockBlogNotification(); + void blockAllContactNotifications(); - void unblockBlogNotification(); + void unblockAllContactNotifications(); + + void blockAllForumPostNotifications(); + + void unblockAllForumPostNotifications(); + + void blockAllBlogPostNotifications(); + + 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 fbed89b0c..15552fdb1 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 143fc1b10..0e89542af 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 e02022ac1..7c093598b 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 7b5e2caa9..4e6d24312 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 afda3f0cf..bead9c4db 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 0f3378852..11c9bd293 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 7bde67e6e..5b768aaa1 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 2ea18075d..82fca3e36 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 ff3ba954a..ba8583fca 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 b6f82ebf5..686d15b61 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() { - @Override - public BluetoothAdapter call() throws Exception { - return BluetoothAdapter.getDefaultAdapter(); - } - }).get(); + adapter = androidExecutor.runOnBackgroundThread( + new Callable() { + @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 07b6e69e5..180881feb 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 Future submit(Callable c) { + @Override + public Future runOnBackgroundThread(Callable c) { FutureTask 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 Future runOnUiThread(Callable c) { + FutureTask 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 a577ac7c3..99ca8fb7a 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); } }