From 209711ae408e50027a816c8fb07395eede4e8c29 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sun, 3 Feb 2013 18:41:34 -0800 Subject: [PATCH] Fix notification behavior. 1) Don't add a notification item to the notification bar if the thread the message is for is active and visible. 2) Only sound the notification ringtone at 1/4th volume if the thread the message is for is active and visible. 3) Auto-clear the notification in the notification bar when a thread becomes visible from a screen-off situation. 4) Make notification updates asynchronous. --- .../securesms/ConversationActivity.java | 22 ++++- .../securesms/ConversationAdapter.java | 24 ------ .../securesms/ConversationListFragment.java | 40 ++++++--- .../securesms/mms/AttachmentManager.java | 81 +++++-------------- .../securesms/service/MessageNotifier.java | 64 +++++++++++++-- .../securesms/service/MmsReceiver.java | 11 ++- .../securesms/service/SmsReceiver.java | 26 +++--- 7 files changed, 150 insertions(+), 118 deletions(-) diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 2fa6fcb56c..718350d94e 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -24,6 +24,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; @@ -132,12 +133,21 @@ public class ConversationActivity extends SherlockFragmentActivity initializeTitleBar(); } + @Override + protected void onPause() { + super.onPause(); + MessageNotifier.setVisibleThread(-1L); + } + @Override protected void onResume() { super.onResume(); initializeSecurity(); initializeTitleBar(); calculateCharactersRemaining(); + + MessageNotifier.setVisibleThread(threadId); + markThreadAsRead(); } @Override @@ -607,6 +617,17 @@ public class ConversationActivity extends SherlockFragmentActivity return rawText; } + private void markThreadAsRead() { + new AsyncTask() { + @Override + protected Void doInBackground(Long... params) { + DatabaseFactory.getThreadDatabase(ConversationActivity.this).setRead(params[0]); + MessageNotifier.updateNotification(ConversationActivity.this); + return null; + } + }.execute(threadId); + } + private void sendComplete(Recipients recipients, long threadId) { attachmentManager.clear(); recipientsPanel.disable(); @@ -651,7 +672,6 @@ public class ConversationActivity extends SherlockFragmentActivity } sendComplete(recipients, allocatedThreadId); - MessageNotifier.updateNotification(ConversationActivity.this, false); } catch (RecipientFormattingException ex) { Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation, diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java index 6dc2f039b9..2b1d330d15 100644 --- a/src/org/thoughtcrime/securesms/ConversationAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java @@ -21,7 +21,6 @@ import android.database.Cursor; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; @@ -43,7 +42,6 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; -import org.thoughtcrime.securesms.service.MessageNotifier; import org.thoughtcrime.securesms.util.InvalidMessageException; import ws.com.google.android.mms.MmsException; @@ -65,7 +63,6 @@ public class ConversationAdapter extends CursorAdapter { private static final int MAX_CACHE_SIZE = 40; - private final TouchListener touchListener = new TouchListener(); private final LinkedHashMap messageRecordCache; private final Handler failedIconClickHandler; private final long threadId; @@ -75,8 +72,6 @@ public class ConversationAdapter extends CursorAdapter { private final MasterCipher masterCipher; private final LayoutInflater inflater; - private boolean dataChanged; - public ConversationAdapter(Recipients recipients, long threadId, Context context, MasterSecret masterSecret, Handler failedIconClickHandler) { @@ -86,14 +81,10 @@ public class ConversationAdapter extends CursorAdapter { this.threadId = threadId; this.masterSecret = masterSecret; this.masterCipher = new MasterCipher(masterSecret); - this.dataChanged = false; this.failedIconClickHandler = failedIconClickHandler; this.messageRecordCache = initializeCache(); this.inflater = (LayoutInflater)context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - DatabaseFactory.getThreadDatabase(context).setRead(threadId); - MessageNotifier.updateNotification(context, false); } @Override @@ -104,8 +95,6 @@ public class ConversationAdapter extends CursorAdapter { MessageRecord messageRecord = getMessageRecord(id, cursor, type); item.set(masterSecret, messageRecord, failedIconClickHandler); - - view.setOnTouchListener(touchListener); } @Override @@ -284,25 +273,12 @@ public class ConversationAdapter extends CursorAdapter { protected void onContentChanged() { super.onContentChanged(); messageRecordCache.clear(); - DatabaseFactory.getThreadDatabase(context).setRead(threadId); - this.dataChanged = true; } public void close() { this.getCursor().close(); } - private class TouchListener implements View.OnTouchListener { - public boolean onTouch(View v, MotionEvent event) { - if (ConversationAdapter.this.dataChanged) { - ConversationAdapter.this.dataChanged = false; - MessageNotifier.updateNotification(context, false); - } - - return false; - } - } - private LinkedHashMap initializeCache() { return new LinkedHashMap() { @Override diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java index cbb81ebe46..8488ccd5c6 100644 --- a/src/org/thoughtcrime/securesms/ConversationListFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationListFragment.java @@ -19,8 +19,10 @@ package org.thoughtcrime.securesms; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; +import android.app.ProgressDialog; import android.content.DialogInterface; import android.database.Cursor; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.support.v4.app.LoaderManager; @@ -32,18 +34,18 @@ import android.widget.AdapterView; import android.widget.CursorAdapter; import android.widget.ListView; -import com.actionbarsherlock.app.SherlockListFragment; -import com.actionbarsherlock.view.ActionMode; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; - import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.loaders.ConversationListLoader; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.service.MessageNotifier; +import com.actionbarsherlock.app.SherlockListFragment; +import com.actionbarsherlock.view.ActionMode; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + import java.util.Set; @@ -157,14 +159,32 @@ public class ConversationListFragment extends SherlockListFragment alert.setCancelable(true); alert.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { - Set selectedConversations = ((ConversationListAdapter)getListAdapter()) + final Set selectedConversations = ((ConversationListAdapter)getListAdapter()) .getBatchSelections(); if (!selectedConversations.isEmpty()) { - DatabaseFactory.getThreadDatabase(getActivity()) - .deleteConversations(selectedConversations); - MessageNotifier.updateNotification(getActivity(), false); + new AsyncTask() { + private ProgressDialog dialog; + + @Override + protected void onPreExecute() { + dialog = ProgressDialog.show(getActivity(), "Deleting", "Deleting selected threads...", true, false); + } + + @Override + protected Void doInBackground(Void... params) { + DatabaseFactory.getThreadDatabase(getActivity()).deleteConversations(selectedConversations); + MessageNotifier.updateNotification(getActivity()); + return null; + } + + @Override + protected void onPostExecute(Void result) { + dialog.dismiss(); + } + }.execute(); } } }); diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index d2b52587b8..a5ff172464 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -1,6 +1,6 @@ -/** +/** * Copyright (C) 2011 Whisper Systems - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -10,100 +10,67 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms.mms; -import java.io.IOException; - -import org.thoughtcrime.securesms.R; - import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ImageView; +import org.thoughtcrime.securesms.R; + +import java.io.IOException; + public class AttachmentManager { private final Context context; private final View attachmentView; private final ImageView thumbnail; - // private final Button viewButton; - // private final Button editButton; private final Button removeButton; private final SlideDeck slideDeck; - + public AttachmentManager(Activity view) { this.attachmentView = (View)view.findViewById(R.id.attachment_editor); this.thumbnail = (ImageView)view.findViewById(R.id.attachment_thumbnail); - // this.viewButton = (Button)view.findViewById(R.id.view_image_button); - // this.editButton = (Button)view.findViewById(R.id.replace_image_button); this.removeButton = (Button)view.findViewById(R.id.remove_image_button); this.slideDeck = new SlideDeck(); - this.context = view; - + this.context = view; + this.removeButton.setOnClickListener(new RemoveButtonListener()); } - + public void clear() { slideDeck.clear(); attachmentView.setVisibility(View.GONE); } - + public void setImage(Uri image) throws IOException { - Log.w("AttachmentManager" , "Setting image: " + image); ImageSlide slide = new ImageSlide(context, image); slideDeck.addSlide(slide); thumbnail.setImageBitmap(slide.getThumbnail()); attachmentView.setVisibility(View.VISIBLE); } - + public void setVideo(Uri video) throws IOException, MediaTooLargeException { VideoSlide slide = new VideoSlide(context, video); slideDeck.addSlide(slide); thumbnail.setImageBitmap(slide.getThumbnail()); attachmentView.setVisibility(View.VISIBLE); } - + public void setAudio(Uri audio)throws IOException, MediaTooLargeException { AudioSlide slide = new AudioSlide(context, audio); slideDeck.addSlide(slide); thumbnail.setImageBitmap(slide.getThumbnail()); attachmentView.setVisibility(View.VISIBLE); } - - - // public Bitmap constructThumbnailFromVideo(Uri uri) { - // MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - // try { - // retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY); - // retriever.setDataSource(context, uri); - // return retriever.captureFrame(); - // } catch (RuntimeException re) { - // Log.w("AttachmentManager", re); - // } finally { - // try { - // retriever.release(); - // } catch (RuntimeException ex) { - // } - // } - // return null; - // } - - // private void displayMessageSizeException() { - // AlertDialog.Builder builder = new AlertDialog.Builder(context); - // builder.setIcon(R.drawable.ic_sms_mms_not_delivered); - // builder.setTitle("Message size limit reached."); - // builder.setMessage("Sorry, this attachment is too large for your message."); - // builder.setPositiveButton("Ok", null); - // builder.show(); - // } public boolean isAttachmentPresent() { return attachmentView.getVisibility() == View.VISIBLE; @@ -112,35 +79,27 @@ public class AttachmentManager { public SlideDeck getSlideDeck() { return slideDeck; } - - // public static void selectAudio(Activity activity, int requestCode) { - // Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); - // intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); - // intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false); - // intent.putExtra(RingtoneManager.EXTRA_RINGTONE_INCLUDE_DRM, false); - // intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, "Select audio"); - // activity.startActivityForResult(intent, requestCode); - // } - + public static void selectVideo(Activity activity, int requestCode) { selectMediaType(activity, "video/*", requestCode); } - + public static void selectImage(Activity activity, int requestCode) { selectMediaType(activity, "image/*", requestCode); } - + public static void selectAudio(Activity activity, int requestCode) { selectMediaType(activity, "audio/*", requestCode); } - + private static void selectMediaType(Activity activity, String type, int requestCode) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType(type); activity.startActivityForResult(intent, requestCode); } - + private class RemoveButtonListener implements View.OnClickListener { + @Override public void onClick(View v) { clear(); } diff --git a/src/org/thoughtcrime/securesms/service/MessageNotifier.java b/src/org/thoughtcrime/securesms/service/MessageNotifier.java index 27789f4c3c..4feb34f070 100644 --- a/src/org/thoughtcrime/securesms/service/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/service/MessageNotifier.java @@ -25,6 +25,8 @@ import android.content.SharedPreferences; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Color; +import android.media.AudioManager; +import android.media.MediaPlayer; import android.net.Uri; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; @@ -43,6 +45,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; +import java.io.IOException; import java.util.LinkedList; /** @@ -56,6 +59,12 @@ public class MessageNotifier { public static final int NOTIFICATION_ID = 1338; + private volatile static long visibleThread = -1; + + public static void setVisibleThread(long threadId) { + visibleThread = threadId; + } + private static Bitmap buildContactPhoto(Recipients recipients) { Recipient recipient = recipients.getPrimaryRecipient(); @@ -176,12 +185,41 @@ public class MessageNotifier { manager.notify(NOTIFICATION_ID, builder.build()); } - private static void flashNotification(Context context, NotificationManager manager) { - sendNotification(context, manager, buildPendingIntent(context, null, null), - null, "(1) New Messages", "(1) New Messages", null, true); + private static void sendInThreadNotification(Context context) { + try { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + String ringtone = sp.getString(ApplicationPreferencesActivity.RINGTONE_PREF, null); + + if (ringtone == null) + return; + + Uri uri = Uri.parse(ringtone); + MediaPlayer player = new MediaPlayer(); + player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); + player.setDataSource(context, uri); + player.setLooping(false); + player.setVolume(0.25f, 0.25f); + player.prepare(); + + final AudioManager audioManager = ((AudioManager)context.getSystemService(Context.AUDIO_SERVICE)); + + audioManager.requestAudioFocus(null, AudioManager.STREAM_NOTIFICATION, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); + + player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + audioManager.abandonAudioFocus(null); + } + }); + + player.start(); + } catch (IOException ioe) { + Log.w("MessageNotifier", ioe); + } } - public static void updateNotification(Context context, boolean signal) { + private static void updateNotification(Context context, boolean signal) { NotificationManager manager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); manager.cancel(NOTIFICATION_ID); @@ -190,8 +228,9 @@ public class MessageNotifier { try { c = DatabaseFactory.getMmsSmsDatabase(context).getUnread(); - if ((c == null && signal) || (!c.moveToFirst() && signal)) {flashNotification(context, manager); return;} - else if (c == null || !c.moveToFirst()) return; + if (c == null || !c.moveToFirst()) { + return; + } Recipients recipients = getMostRecentRecipients(context, c); String ticker = buildTickerMessage(context, c.getCount(), recipients); @@ -208,6 +247,19 @@ public class MessageNotifier { } } + public static void updateNotification(final Context context) { + updateNotification(context, false); + } + + public static void updateNotification(Context context, long threadId) { + if (visibleThread == threadId) { + DatabaseFactory.getThreadDatabase(context).setRead(threadId); + sendInThreadNotification(context); + } else { + updateNotification(context, true); + } + } + private static String[] parseBlinkPattern(String blinkPattern, String blinkPatternCustom) { if (blinkPattern.equals("custom")) blinkPattern = blinkPatternCustom; diff --git a/src/org/thoughtcrime/securesms/service/MmsReceiver.java b/src/org/thoughtcrime/securesms/service/MmsReceiver.java index 4b2f53a40d..1d3552679b 100644 --- a/src/org/thoughtcrime/securesms/service/MmsReceiver.java +++ b/src/org/thoughtcrime/securesms/service/MmsReceiver.java @@ -37,12 +37,12 @@ public class MmsReceiver { this.context = context; } - private void scheduleDownload(NotificationInd pdu, long messageId) { + private void scheduleDownload(NotificationInd pdu, long messageId, long threadId) { Intent intent = new Intent(SendReceiveService.DOWNLOAD_MMS_ACTION, null, context, SendReceiveService.class); intent.putExtra("content_location", new String(pdu.getContentLocation())); intent.putExtra("message_id", messageId); intent.putExtra("transaction_id", pdu.getTransactionId()); - intent.putExtra("thread_id", DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); + intent.putExtra("thread_id", threadId); context.startService(intent); } @@ -61,8 +61,11 @@ public class MmsReceiver { database = DatabaseFactory.getMmsDatabase(context); long messageId = database.insertMessageReceived((NotificationInd)pdu); - MessageNotifier.updateNotification(context, true); - scheduleDownload((NotificationInd)pdu, messageId); + long threadId = database.getThreadIdForMessage(messageId); + + MessageNotifier.updateNotification(context, threadId); + scheduleDownload((NotificationInd)pdu, messageId, threadId); + Log.w("MmsReceiverService", "Inserted received notification..."); } } diff --git a/src/org/thoughtcrime/securesms/service/SmsReceiver.java b/src/org/thoughtcrime/securesms/service/SmsReceiver.java index 870f417897..98e39171cd 100644 --- a/src/org/thoughtcrime/securesms/service/SmsReceiver.java +++ b/src/org/thoughtcrime/securesms/service/SmsReceiver.java @@ -86,11 +86,13 @@ public class SmsReceiver { } } - private void storeSecureMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) { + private long storeSecureMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) { long messageId = DatabaseFactory.getSmsDatabase(context).insertSecureMessageReceived(message, messageBody); Log.w("SmsReceiver", "Inserted secure message received: " + messageId); if (masterSecret != null) DecryptingQueue.scheduleDecryption(context, masterSecret, messageId, message.getDisplayOriginatingAddress(), messageBody); + + return messageId; } private long storeStandardMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) { @@ -99,7 +101,7 @@ public class SmsReceiver { else return DatabaseFactory.getSmsDatabase(context).insertMessageReceived(message, messageBody); } - private void storeKeyExchangeMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) { + private long storeKeyExchangeMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) { if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(ApplicationPreferencesActivity.AUTO_KEY_EXCHANGE_PREF, true)) { try { Recipient recipient = new Recipient(null, message.getDisplayOriginatingAddress(), null, null); @@ -118,7 +120,7 @@ public class SmsReceiver { long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId); processor.processKeyExchangeMessage(keyExchangeMessage, threadId); - return; + return messageId; } } catch (InvalidVersionException e) { Log.w("SmsReceiver", e); @@ -127,19 +129,17 @@ public class SmsReceiver { } } - storeStandardMessage(masterSecret, message, messageBody); + return storeStandardMessage(masterSecret, message, messageBody); } - private boolean storeMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) { + private long storeMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) { if (messageBody.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) { - storeSecureMessage(masterSecret, message, messageBody); + return storeSecureMessage(masterSecret, message, messageBody); } else if (messageBody.startsWith(Prefix.KEY_EXCHANGE)) { - storeKeyExchangeMessage(masterSecret, message, messageBody); + return storeKeyExchangeMessage(masterSecret, message, messageBody); } else { - storeStandardMessage(masterSecret, message, messageBody); + return storeStandardMessage(masterSecret, message, messageBody); } - - return true; } private SmsMessage[] parseMessages(Bundle bundle) { @@ -158,8 +158,10 @@ public class SmsReceiver { String message = assembleMessageFragments(messages); if (message != null) { - storeMessage(masterSecret, messages[0], message); - MessageNotifier.updateNotification(context, true); + long messageId = storeMessage(masterSecret, messages[0], message); + long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId); + + MessageNotifier.updateNotification(context, threadId); } }