diff --git a/res/values/strings.xml b/res/values/strings.xml index 1aa775b482..90054a114f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -208,7 +208,7 @@ Decrypting, please wait... Message encrypted for non-existing session... Decryption error: local message corrupted, MAC doesn\'t match. Potential tampering? - + Connecting to MMS server... Downloading MMS... @@ -230,9 +230,13 @@ Passphrase Cached - (%d) New messages - (%1$d) New messages, most recent from: %2$s - Most recent from: %s + %d new messages + Most recent from: %s + Key exchange... + Encrypted message... + Corrupted ciphertext + (No Subject) + You have received a message from someone who supports TextSecure encrypted sessions. Would you like to initiate a secure session? diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index f4d1351040..9b8c99150f 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -60,12 +60,12 @@ import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter; import org.thoughtcrime.securesms.mms.MediaTooLargeException; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; +import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.protocol.Tag; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.service.MessageNotifier; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.CharacterCalculator; import org.thoughtcrime.securesms.util.EncryptedCharacterCalculator; @@ -540,7 +540,7 @@ public class ConversationActivity extends SherlockFragmentActivity }; registerReceiver(killActivityReceiver, - new IntentFilter(KeyCachingService.PASSPHRASE_EXPIRED_EVENT), + new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT), KeyCachingService.KEY_PERMISSION, null); registerReceiver(securityUpdateReceiver, @@ -703,7 +703,7 @@ public class ConversationActivity extends SherlockFragmentActivity @Override protected Void doInBackground(Long... params) { DatabaseFactory.getThreadDatabase(ConversationActivity.this).setRead(params[0]); - MessageNotifier.updateNotification(ConversationActivity.this); + MessageNotifier.updateNotification(ConversationActivity.this, masterSecret); return null; } }.execute(threadId); diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index ccec259080..ed7fca6605 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -48,6 +48,7 @@ public class ConversationListActivity extends SherlockFragmentActivity private ApplicationMigrationManager migrationManager; private boolean havePromptedForPassphrase = false; + private boolean isVisible = false; @Override public void onCreate(Bundle icicle) { @@ -75,6 +76,8 @@ public class ConversationListActivity extends SherlockFragmentActivity unregisterReceiver(newKeyReceiver); newKeyReceiver = null; } + + isVisible = false; } @Override @@ -83,6 +86,7 @@ public class ConversationListActivity extends SherlockFragmentActivity clearNotifications(); initializeKeyCachingServiceRegistration(); + isVisible = true; } @Override @@ -199,15 +203,9 @@ public class ConversationListActivity extends SherlockFragmentActivity } private void handleClearPassphrase() { - Intent keyService = new Intent(this, KeyCachingService.class); - - keyService.setAction(KeyCachingService.CLEAR_KEY_ACTION); - startService(keyService); - - this.masterSecret = null; - fragment.setMasterSecret(null); - - promptForPassphrase(); + Intent intent = new Intent(this, KeyCachingService.class); + intent.setAction(KeyCachingService.CLEAR_KEY_ACTION); + startService(intent); } private void initializeWithMasterSecret(MasterSecret masterSecret) { @@ -235,12 +233,17 @@ public class ConversationListActivity extends SherlockFragmentActivity this.killActivityReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - finish(); + ConversationListActivity.this.masterSecret = null; + fragment.setMasterSecret(null); + + if (isVisible) { + promptForPassphrase(); + } } }; registerReceiver(this.killActivityReceiver, - new IntentFilter(KeyCachingService.PASSPHRASE_EXPIRED_EVENT), + new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT), KeyCachingService.KEY_PERMISSION, null); } diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java index 72f1b5f749..558b810104 100644 --- a/src/org/thoughtcrime/securesms/ConversationListFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationListFragment.java @@ -37,8 +37,8 @@ import android.widget.ListView; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.loaders.ConversationListLoader; +import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipients; -import org.thoughtcrime.securesms.service.MessageNotifier; import com.actionbarsherlock.app.SherlockListFragment; import com.actionbarsherlock.view.ActionMode; @@ -179,7 +179,7 @@ public class ConversationListFragment extends SherlockListFragment @Override protected Void doInBackground(Void... params) { DatabaseFactory.getThreadDatabase(getActivity()).deleteConversations(selectedConversations); - MessageNotifier.updateNotification(getActivity()); + MessageNotifier.updateNotification(getActivity(), masterSecret); return null; } diff --git a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java index e06c761f77..e738840f55 100644 --- a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java +++ b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java @@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.database.EncryptingMmsDatabase; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.mms.TextTransport; +import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.protocol.Prefix; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; @@ -145,6 +146,7 @@ public class DecryptingQueue { return null; } + @Override public void run() { EncryptingMmsDatabase database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret); @@ -178,7 +180,6 @@ public class DecryptingQueue { Log.w("DecryptingQueue", "Successfully decrypted MMS!"); database.insertSecureDecryptedMessageReceived(plaintextPdu, threadId); database.delete(messageId); - } catch (RecipientFormattingException rfe) { Log.w("DecryptingQueue", rfe); database.markAsDecryptFailed(messageId, threadId); @@ -240,6 +241,7 @@ public class DecryptingQueue { } database.updateSecureMessageBody(masterSecret, messageId, plaintextBody); + MessageNotifier.updateNotification(context, masterSecret); } private void handleLocalAsymmetricEncrypt() { @@ -261,8 +263,10 @@ public class DecryptingQueue { } database.updateMessageBody(masterSecret, messageId, plaintextBody); + MessageNotifier.updateNotification(context, masterSecret); } + @Override public void run() { if (body.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) handleRemoteAsymmetricEncrypt(); else if (body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT)) handleLocalAsymmetricEncrypt(); diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java new file mode 100644 index 0000000000..9b555d6a88 --- /dev/null +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -0,0 +1,321 @@ +/** + * 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 + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms.notifications; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.graphics.BitmapFactory; +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; +import android.support.v4.app.NotificationCompat.BigTextStyle; +import android.support.v4.app.NotificationCompat.InboxStyle; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.util.Log; + +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; +import org.thoughtcrime.securesms.ConversationListActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.MasterCipher; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.crypto.MessageDisplayHelper; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.protocol.Prefix; +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.util.InvalidMessageException; +import org.thoughtcrime.securesms.util.Util; + +import java.io.IOException; +import java.util.List; + +/** + * Handles posting system notifications for new messages. + * + * + * @author Moxie Marlinspike + */ + +public class MessageNotifier { + + public static final int NOTIFICATION_ID = 1338; + + private volatile static long visibleThread = -1; + + public static void setVisibleThread(long threadId) { + visibleThread = threadId; + } + + public static void updateNotification(Context context, MasterSecret masterSecret) { + updateNotification(context, masterSecret, false); + } + + public static void updateNotification(Context context, MasterSecret masterSecret, long threadId) { + if (visibleThread == threadId) { + DatabaseFactory.getThreadDatabase(context).setRead(threadId); + sendInThreadNotification(context); + } else { + updateNotification(context, masterSecret, true); + } + } + + private static void updateNotification(Context context, MasterSecret masterSecret, boolean signal) { + Cursor cursor = null; + + try { + cursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread(); + + if (cursor == null || cursor.isAfterLast()) { + ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) + .cancel(NOTIFICATION_ID); + return; + } + + NotificationState notificationState = constructNotificationState(context, masterSecret, cursor); + + if (notificationState.hasMultipleThreads()) { + sendMultipleThreadNotification(context, notificationState, signal); + } else { + sendSingleThreadNotification(context, notificationState, signal); + } + } finally { + if (cursor != null) + cursor.close(); + } + } + + private static void sendSingleThreadNotification(Context context, + NotificationState notificationState, + boolean signal) + { + List notifications = notificationState.getNotifications(); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context); + Recipients recipients = notifications.get(0).getRecipients(); + + builder.setSmallIcon(R.drawable.icon_notification); + builder.setLargeIcon(recipients.getPrimaryRecipient().getContactPhoto()); + builder.setContentTitle(recipients.getPrimaryRecipient().toShortString()); + builder.setContentText(notifications.get(0).getText()); + builder.setContentIntent(notifications.get(0).getPendingIntent(context)); + + SpannableStringBuilder content = new SpannableStringBuilder(); + + for (NotificationItem item : notifications) { + content.append(item.getBigStyleSummary()); + content.append('\n'); + } + + builder.setStyle(new BigTextStyle().bigText(content)); + + setNotificationAlarms(context, builder, signal); + + if (signal) { + builder.setTicker(notifications.get(0).getTickerText()); + } + + ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(NOTIFICATION_ID, builder.build()); + } + + private static void sendMultipleThreadNotification(Context context, + NotificationState notificationState, + boolean signal) + { + List notifications = notificationState.getNotifications(); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context); + + builder.setSmallIcon(R.drawable.icon_notification); + builder.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), + R.drawable.icon_notification)); + builder.setContentTitle(String.format(context.getString(R.string.MessageNotifier_d_new_messages), + notificationState.getMessageCount())); + builder.setContentText(String.format(context.getString(R.string.MessageNotifier_most_recent_from_s), + notifications.get(0).getRecipientName())); + builder.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0)); + + InboxStyle style = new InboxStyle(); + + for (NotificationItem item : notifications) { + style.addLine(item.getTickerText()); + } + + builder.setStyle(style); + + setNotificationAlarms(context, builder, signal); + + if (signal) { + builder.setTicker(notifications.get(0).getTickerText()); + } + + ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(NOTIFICATION_ID, builder.build()); + } + + 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); + } + } + + private static NotificationState constructNotificationState(Context context, + MasterSecret masterSecret, + Cursor cursor) + { + NotificationState notificationState = new NotificationState(); + + while (cursor.moveToNext()) { + Recipients recipients = getRecipients(context, cursor); + long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.THREAD_ID)); + CharSequence body = getBody(context, masterSecret, cursor); + Uri image = null; + + notificationState.addNotification(new NotificationItem(recipients, threadId, body, image)); + } + + return notificationState; + } + + private static CharSequence getBody(Context context, MasterSecret masterSecret, Cursor cursor) { + String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)); + + if (body == null) { + return context.getString(R.string.MessageNotifier_no_subject); + } + + if (masterSecret != null) { + try { + body = MessageDisplayHelper.getDecryptedMessageBody(new MasterCipher(masterSecret), body); + } catch (InvalidMessageException e) { + Log.w("MessageNotifier", e); + return Util.getItalicizedString(context.getString(R.string.MessageNotifier_corrupted_ciphertext)); + } + } + + if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT) || + body.startsWith(Prefix.ASYMMETRIC_ENCRYPT) || + body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT)) + { + return Util.getItalicizedString(context.getString(R.string.MessageNotifier_encrypted_message)); + } else if (body.startsWith(Prefix.KEY_EXCHANGE) || + body.startsWith(Prefix.PROCESSED_KEY_EXCHANGE)) + { + return Util.getItalicizedString(context.getString(R.string.MessageNotifier_key_exchange)); + } + + return body; + } + + private static Recipients getSmsRecipient(Context context, Cursor cursor) + throws RecipientFormattingException + { + String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS)); + return RecipientFactory.getRecipientsFromString(context, address, false); + } + + private static Recipients getMmsRecipient(Context context, Cursor cursor) + throws RecipientFormattingException + { + long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID)); + String address = DatabaseFactory.getMmsDatabase(context).getMessageRecipient(messageId); + return RecipientFactory.getRecipientsFromString(context, address, false); + } + + private static Recipients getRecipients(Context context, Cursor cursor) { + String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); + + try { + if (type.equals("sms")) { + return getSmsRecipient(context, cursor); + } else { + return getMmsRecipient(context, cursor); + } + } catch (RecipientFormattingException e) { + return new Recipients(new Recipient("Unknown", null, null)); + } + } + + private static void setNotificationAlarms(Context context, + NotificationCompat.Builder builder, + boolean signal) + { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + + String ringtone = sp.getString(ApplicationPreferencesActivity.RINGTONE_PREF, null); + boolean vibrate = sp.getBoolean(ApplicationPreferencesActivity.VIBRATE_PREF, true); + String ledColor = sp.getString(ApplicationPreferencesActivity.LED_COLOR_PREF, "green"); + String ledBlinkPattern = sp.getString(ApplicationPreferencesActivity.LED_BLINK_PREF, "500,2000"); + String ledBlinkPatternCustom = sp.getString(ApplicationPreferencesActivity.LED_BLINK_PREF_CUSTOM, "500,2000"); + String[] blinkPatternArray = parseBlinkPattern(ledBlinkPattern, ledBlinkPatternCustom); + + builder.setSound(TextUtils.isEmpty(ringtone) || !signal ? null : Uri.parse(ringtone)); + + if (signal && vibrate) + builder.setDefaults(Notification.DEFAULT_VIBRATE); + + builder.setLights(Color.parseColor(ledColor), Integer.parseInt(blinkPatternArray[0]), + Integer.parseInt(blinkPatternArray[1])); + } + + private static String[] parseBlinkPattern(String blinkPattern, String blinkPatternCustom) { + if (blinkPattern.equals("custom")) + blinkPattern = blinkPatternCustom; + + return blinkPattern.split(","); + } +} diff --git a/src/org/thoughtcrime/securesms/notifications/NotificationItem.java b/src/org/thoughtcrime/securesms/notifications/NotificationItem.java new file mode 100644 index 0000000000..50a4ac9814 --- /dev/null +++ b/src/org/thoughtcrime/securesms/notifications/NotificationItem.java @@ -0,0 +1,78 @@ +package org.thoughtcrime.securesms.notifications; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.SpannableStringBuilder; + +import org.thoughtcrime.securesms.ConversationListActivity; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.Util; + +public class NotificationItem { + + private final Recipients recipients; + private final long threadId; + private final CharSequence text; + private final Uri image; + + public NotificationItem(Recipients recipients, long threadId, CharSequence text, Uri image) { + this.recipients = recipients; + this.text = text; + this.image = image; + this.threadId = threadId; + } + + public Recipients getRecipients() { + return recipients; + } + + public String getRecipientName() { + return recipients.getPrimaryRecipient().toShortString(); + } + + public CharSequence getText() { + return text; + } + + public Uri getImage() { + return image; + } + + public boolean hasImage() { + return image != null; + } + + public long getThreadId() { + return threadId; + } + + public CharSequence getBigStyleSummary() { + return (text == null) ? "" : text; + } + + public CharSequence getTickerText() { + SpannableStringBuilder builder = new SpannableStringBuilder(); + builder.append(Util.getBoldedString(getRecipientName())); + builder.append(": "); + builder.append(getText()); + + return builder; + } + + public PendingIntent getPendingIntent(Context context) { + Intent intent = new Intent(context, ConversationListActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + + if (recipients.getPrimaryRecipient() != null) { + intent.putExtra("recipients", recipients); + intent.putExtra("thread_id", threadId); + } + + intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); + + return PendingIntent.getActivity(context, 0, intent, 0); + } + +} diff --git a/src/org/thoughtcrime/securesms/notifications/NotificationState.java b/src/org/thoughtcrime/securesms/notifications/NotificationState.java new file mode 100644 index 0000000000..237354de61 --- /dev/null +++ b/src/org/thoughtcrime/securesms/notifications/NotificationState.java @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.notifications; + +import android.graphics.Bitmap; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +public class NotificationState { + + private final LinkedList notifications = new LinkedList(); + private final Set threads = new HashSet(); + + private int notificationCount = 0; + + public void addNotification(NotificationItem item) { + notifications.addFirst(item); + threads.add(item.getThreadId()); + notificationCount++; + } + + public boolean hasMultipleThreads() { + return threads.size() > 1; + } + + public int getMessageCount() { + return notificationCount; + } + + public List getNotifications() { + return notifications; + } + + public Bitmap getContactPhoto() { + return notifications.get(0).getRecipients().getPrimaryRecipient().getContactPhoto(); + } + +} diff --git a/src/org/thoughtcrime/securesms/service/KeyCachingService.java b/src/org/thoughtcrime/securesms/service/KeyCachingService.java index b33e553d07..d8b9283781 100644 --- a/src/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/src/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.notifications.MessageNotifier; /** * Small service that stays running to keep a key cached in memory. @@ -48,7 +49,8 @@ public class KeyCachingService extends Service { public static final String KEY_PERMISSION = "org.thoughtcrime.securesms.ACCESS_SECRETS"; public static final String NEW_KEY_EVENT = "org.thoughtcrime.securesms.service.action.NEW_KEY_EVENT"; - public static final String PASSPHRASE_EXPIRED_EVENT = "org.thoughtcrime.securesms.service.action.PASSPHRASE_EXPIRED_EVENT"; + public static final String CLEAR_KEY_EVENT = "org.thoughtcrime.securesms.service.action.CLEAR_KEY_EVENT"; + private static final String PASSPHRASE_EXPIRED_EVENT = "org.thoughtcrime.securesms.service.action.PASSPHRASE_EXPIRED_EVENT"; public static final String CLEAR_KEY_ACTION = "org.thoughtcrime.securesms.service.action.CLEAR_KEY"; public static final String ACTIVITY_START_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_START_EVENT"; public static final String ACTIVITY_STOP_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_STOP_EVENT"; @@ -67,12 +69,19 @@ public class KeyCachingService extends Service { return masterSecret; } - public synchronized void setMasterSecret(MasterSecret masterSecret) { + public synchronized void setMasterSecret(final MasterSecret masterSecret) { this.masterSecret = masterSecret; foregroundService(); broadcastNewSecret(); startTimeoutIfAppropriate(); + + new Thread() { + @Override + public void run() { + MessageNotifier.updateNotification(KeyCachingService.this, masterSecret); + } + }.start(); } @Override @@ -86,17 +95,20 @@ public class KeyCachingService extends Service { else if (intent.getAction() != null && intent.getAction().equals(ACTIVITY_STOP_EVENT)) handleActivityStopped(); else if (intent.getAction() != null && intent.getAction().equals(PASSPHRASE_EXPIRED_EVENT)) - handlePassphraseExpired(); + handleClearKey(); } @Override public void onCreate() { + super.onCreate(); pending = PendingIntent.getService(this, 0, new Intent(PASSPHRASE_EXPIRED_EVENT, null, this, KeyCachingService.class), 0); } @Override public void onDestroy() { - Log.e("kcs", "KCS Is Being Destroyed!"); + super.onDestroy(); + Log.w("KeyCachingService", "KCS Is Being Destroyed!"); + handleClearKey(); } private void handleActivityStarted() { @@ -117,14 +129,18 @@ public class KeyCachingService extends Service { private void handleClearKey() { this.masterSecret = null; stopForeground(true); - } - private void handlePassphraseExpired() { - handleClearKey(); - Intent intent = new Intent(PASSPHRASE_EXPIRED_EVENT); + Intent intent = new Intent(CLEAR_KEY_EVENT); intent.setPackage(getApplicationContext().getPackageName()); sendBroadcast(intent, KEY_PERMISSION); + + new Thread() { + @Override + public void run() { + MessageNotifier.updateNotification(KeyCachingService.this, null); + } + }.start(); } private void startTimeoutIfAppropriate() { @@ -180,6 +196,7 @@ public class KeyCachingService extends Service { private void broadcastNewSecret() { Log.w("service", "Broadcasting new secret..."); + Intent intent = new Intent(NEW_KEY_EVENT); intent.putExtra("master_secret", masterSecret); intent.setPackage(getApplicationContext().getPackageName()); diff --git a/src/org/thoughtcrime/securesms/service/MessageNotifier.java b/src/org/thoughtcrime/securesms/service/MessageNotifier.java deleted file mode 100644 index 4feb34f070..0000000000 --- a/src/org/thoughtcrime/securesms/service/MessageNotifier.java +++ /dev/null @@ -1,269 +0,0 @@ -/** - * 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 - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.service; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -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; -import android.text.TextUtils; -import android.util.Log; - -import org.thoughtcrime.securesms.ApplicationPreferencesActivity; -import org.thoughtcrime.securesms.ConversationListActivity; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.MmsSmsDatabase; -import org.thoughtcrime.securesms.database.SmsDatabase; -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 java.io.IOException; -import java.util.LinkedList; - -/** - * Handles posting system notifications for new messages. - * - * - * @author Moxie Marlinspike - */ - -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(); - - if (recipient == null) { - return null; - } else { - return recipient.getContactPhoto(); - } - } - - private static String buildTickerMessage(Context context, int count, Recipients recipients) { - Recipient recipient = recipients.getPrimaryRecipient(); - - if (recipient == null) { - return String.format(context.getString(R.string.MessageNotifier_d_new_messages), count); - } else { - return String.format(context.getString(R.string.MessageNotifier_d_new_messages_most_recent_from_s), count, - recipient.getName() == null ? recipient.getNumber() : recipient.getName()); - } - } - - private static String buildTitleMessage(Context context, int count) { - return String.format(context.getString(R.string.MessageNotifier_d_new_messages), count); - } - - private static String buildSubtitleMessage(Context context, Recipients recipients) { - Recipient recipient = recipients.getPrimaryRecipient(); - - if (recipient != null) { - return String.format(context.getString(R.string.MessageNotifier_most_recent_from_s), - (recipient.getName() == null ? recipient.getNumber() : recipient.getName())); - } - - return null; - } - - private static Recipients getSmsRecipient(Context context, Cursor c) throws RecipientFormattingException { - String address = c.getString(c.getColumnIndexOrThrow(SmsDatabase.ADDRESS)); - return RecipientFactory.getRecipientsFromString(context, address, false); - } - - private static Recipients getMmsRecipient(Context context, Cursor c) throws RecipientFormattingException { - long messageId = c.getLong(c.getColumnIndexOrThrow(MmsDatabase.ID)); - String address = DatabaseFactory.getMmsDatabase(context).getMessageRecipient(messageId); - return RecipientFactory.getRecipientsFromString(context, address, false); - } - - private static Recipients getMostRecentRecipients(Context context, Cursor c) { - if (c != null && c.moveToLast()) { - try { - String type = c.getString(c.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); - - if (type.equals("sms")) - return getSmsRecipient(context, c); - else - return getMmsRecipient(context, c); - - } catch (RecipientFormattingException e) { - return new Recipients(new LinkedList()); - } - } - - return null; - } - - - private static PendingIntent buildPendingIntent(Context context, Cursor c, Recipients recipients) { - Intent intent = new Intent(context, ConversationListActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - - Log.w("SMSNotifier", "Building pending intent..."); - if (c != null && c.getCount() == 1) { - Log.w("SMSNotifier", "Adding extras..."); - c.moveToLast(); - long threadId = c.getLong(c.getColumnIndexOrThrow(SmsDatabase.THREAD_ID)); - Log.w("SmsNotifier", "Adding thread_id to pending intent: " + threadId); - - if (recipients.getPrimaryRecipient() != null) { - intent.putExtra("recipients", recipients); - intent.putExtra("thread_id", threadId); - } - - intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); - } - - return PendingIntent.getActivity(context, 0, intent, 0); - } - - private static void sendNotification(Context context, NotificationManager manager, - PendingIntent launchIntent, Bitmap contactPhoto, - String ticker, String title, - String subtitle, boolean signal) - { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - if (!sp.getBoolean(ApplicationPreferencesActivity.NOTIFICATION_PREF, true)) return; - - String ringtone = sp.getString(ApplicationPreferencesActivity.RINGTONE_PREF, null); - boolean vibrate = sp.getBoolean(ApplicationPreferencesActivity.VIBRATE_PREF, true); - String ledColor = sp.getString(ApplicationPreferencesActivity.LED_COLOR_PREF, "green"); - String ledBlinkPattern = sp.getString(ApplicationPreferencesActivity.LED_BLINK_PREF, "500,2000"); - String ledBlinkPatternCustom = sp.getString(ApplicationPreferencesActivity.LED_BLINK_PREF_CUSTOM, "500,2000"); - String[] blinkPatternArray = parseBlinkPattern(ledBlinkPattern, ledBlinkPatternCustom); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context); - builder.setSmallIcon(R.drawable.icon_notification); - builder.setLargeIcon(contactPhoto); - builder.setTicker(ticker); - builder.setContentTitle(title); - builder.setContentText(subtitle); - builder.setContentIntent(launchIntent); - builder.setSound(TextUtils.isEmpty(ringtone) || !signal ? null : Uri.parse(ringtone)); - - if (signal && vibrate) - builder.setDefaults(Notification.DEFAULT_VIBRATE); - - builder.setLights(Color.parseColor(ledColor), Integer.parseInt(blinkPatternArray[0]), Integer.parseInt(blinkPatternArray[1])); - - manager.notify(NOTIFICATION_ID, builder.build()); - } - - 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); - } - } - - private static void updateNotification(Context context, boolean signal) { - NotificationManager manager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); - manager.cancel(NOTIFICATION_ID); - - Cursor c = null; - - try { - c = DatabaseFactory.getMmsSmsDatabase(context).getUnread(); - - if (c == null || !c.moveToFirst()) { - return; - } - - Recipients recipients = getMostRecentRecipients(context, c); - String ticker = buildTickerMessage(context, c.getCount(), recipients); - String title = buildTitleMessage(context, c.getCount()); - String subtitle = buildSubtitleMessage(context, recipients); - PendingIntent launchIntent = buildPendingIntent(context, c, recipients); - Bitmap contactPhoto = buildContactPhoto(recipients); - - sendNotification(context, manager, launchIntent, contactPhoto, - ticker, title, subtitle, signal); - } finally { - if (c != null) - c.close(); - } - } - - 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; - - return blinkPattern.split(","); - } -} diff --git a/src/org/thoughtcrime/securesms/service/MmsReceiver.java b/src/org/thoughtcrime/securesms/service/MmsReceiver.java index 1d3552679b..32a7248318 100644 --- a/src/org/thoughtcrime/securesms/service/MmsReceiver.java +++ b/src/org/thoughtcrime/securesms/service/MmsReceiver.java @@ -23,6 +23,7 @@ import android.util.Log; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.notifications.MessageNotifier; import ws.com.google.android.mms.pdu.GenericPdu; import ws.com.google.android.mms.pdu.NotificationInd; @@ -63,7 +64,7 @@ public class MmsReceiver { long messageId = database.insertMessageReceived((NotificationInd)pdu); long threadId = database.getThreadIdForMessage(messageId); - MessageNotifier.updateNotification(context, threadId); + MessageNotifier.updateNotification(context, masterSecret, threadId); scheduleDownload((NotificationInd)pdu, messageId, threadId); Log.w("MmsReceiverService", "Inserted received notification..."); diff --git a/src/org/thoughtcrime/securesms/service/SendReceiveService.java b/src/org/thoughtcrime/securesms/service/SendReceiveService.java index 50c0b6e1d1..fb51e8eb51 100644 --- a/src/org/thoughtcrime/securesms/service/SendReceiveService.java +++ b/src/org/thoughtcrime/securesms/service/SendReceiveService.java @@ -70,13 +70,16 @@ public class SendReceiveService extends Service { private MmsDownloader mmsDownloader; private MasterSecret masterSecret; - private NewKeyReceiver receiver; + private boolean hasSecret; + + private NewKeyReceiver newKeyReceiver; + private ClearKeyReceiver clearKeyReceiver; private List workQueue; private List pendingSecretList; private Thread workerThread; @Override - public void onCreate() { + public void onCreate() { initializeHandlers(); initializeProcessors(); initializeAddressCanonicalization(); @@ -111,6 +114,18 @@ public class SendReceiveService extends Service { return null; } + @Override + public void onDestroy() { + Log.w("SendReceiveService", "onDestroy()..."); + super.onDestroy(); + + if (newKeyReceiver != null) + unregisterReceiver(newKeyReceiver); + + if (clearKeyReceiver != null) + unregisterReceiver(clearKeyReceiver); + } + private void initializeHandlers() { toastHandler = new ToastHandler(); } @@ -132,20 +147,27 @@ public class SendReceiveService extends Service { } private void initializeMasterSecret() { - receiver = new NewKeyReceiver(); - IntentFilter filter = new IntentFilter(KeyCachingService.NEW_KEY_EVENT); - registerReceiver(receiver, filter, KeyCachingService.KEY_PERMISSION, null); + hasSecret = false; + newKeyReceiver = new NewKeyReceiver(); + clearKeyReceiver = new ClearKeyReceiver(); + + IntentFilter newKeyFilter = new IntentFilter(KeyCachingService.NEW_KEY_EVENT); + registerReceiver(newKeyReceiver, newKeyFilter, KeyCachingService.KEY_PERMISSION, null); + + IntentFilter clearKeyFilter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT); + registerReceiver(clearKeyReceiver, clearKeyFilter, KeyCachingService.KEY_PERMISSION, null); Intent bindIntent = new Intent(this, KeyCachingService.class); bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE); } private void initializeWithMasterSecret(MasterSecret masterSecret) { - Log.w("SendReceiveService", "SendReceive service got master secret: " + masterSecret); + Log.w("SendReceiveService", "SendReceive service got master secret."); if (masterSecret != null) { synchronized (workQueue) { this.masterSecret = masterSecret; + this.hasSecret = true; Iterator iterator = pendingSecretList.iterator(); while (iterator.hasNext()) @@ -173,7 +195,7 @@ public class SendReceiveService extends Service { Runnable work = new SendReceiveWorkItem(intent, what); synchronized (workQueue) { - if (masterSecret != null) { + if (hasSecret) { workQueue.add(work); workQueue.notifyAll(); } else { @@ -183,7 +205,6 @@ public class SendReceiveService extends Service { } private class SendReceiveWorkItem implements Runnable { - private final Intent intent; private final int what; @@ -192,6 +213,7 @@ public class SendReceiveService extends Service { this.what = what; } + @Override public void run() { switch (what) { case RECEIVE_SMS: smsReceiver.process(masterSecret, intent); return; @@ -210,12 +232,13 @@ public class SendReceiveService extends Service { this.sendMessage(message); } @Override - public void handleMessage(Message message) { + public void handleMessage(Message message) { Toast.makeText(SendReceiveService.this, (String)message.obj, Toast.LENGTH_LONG).show(); } } private ServiceConnection serviceConnection = new ServiceConnection() { + @Override public void onServiceConnected(ComponentName className, IBinder service) { KeyCachingService keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService(); MasterSecret masterSecret = keyCachingService.getMasterSecret(); @@ -225,6 +248,7 @@ public class SendReceiveService extends Service { SendReceiveService.this.unbindService(this); } + @Override public void onServiceDisconnected(ComponentName name) {} }; @@ -234,6 +258,48 @@ public class SendReceiveService extends Service { Log.w("SendReceiveService", "Got a MasterSecret broadcast..."); initializeWithMasterSecret((MasterSecret)intent.getParcelableExtra("master_secret")); } - }; + } + + /** + * This class receives broadcast notifications to clear the MasterSecret. + * + * We don't want to clear it immediately, since there are potentially jobs + * in the work queue which require the master secret. Instead, we reset a + * flag so that new incoming jobs will be evaluated as if no mastersecret is + * present. + * + * Then, we add a job to the end of the queue which actually clears the masterSecret + * value. That way all jobs before this moment will be processed correctly, and all + * jobs after this moment will be evaluated as if no mastersecret is present (and potentially + * held). + * + * When we go to actually clear the mastersecret, we ensure that the flag is still false. + * This allows a new mastersecret broadcast to come in correctly without us clobbering it. + * + */ + private class ClearKeyReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Log.w("SendReceiveService", "Got a clear mastersecret broadcast..."); + + synchronized (workQueue) { + SendReceiveService.this.hasSecret = false; + workQueue.add(new Runnable() { + @Override + public void run() { + Log.w("SendReceiveService", "Running clear key work item..."); + + synchronized (workQueue) { + if (!SendReceiveService.this.hasSecret) { + Log.w("SendReceiveService", "Actually clearing key..."); + SendReceiveService.this.masterSecret = null; + } + } + } + }); + workQueue.notifyAll(); + } + } + }; } diff --git a/src/org/thoughtcrime/securesms/service/SmsReceiver.java b/src/org/thoughtcrime/securesms/service/SmsReceiver.java index 98e39171cd..8388a938bc 100644 --- a/src/org/thoughtcrime/securesms/service/SmsReceiver.java +++ b/src/org/thoughtcrime/securesms/service/SmsReceiver.java @@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.protocol.Prefix; import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.recipients.Recipient; @@ -161,7 +162,7 @@ public class SmsReceiver { long messageId = storeMessage(masterSecret, messages[0], message); long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId); - MessageNotifier.updateNotification(context, threadId); + MessageNotifier.updateNotification(context, masterSecret, threadId); } } diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java index 05fe61b45b..5729d329d3 100644 --- a/src/org/thoughtcrime/securesms/util/Util.java +++ b/src/org/thoughtcrime/securesms/util/Util.java @@ -16,6 +16,10 @@ */ package org.thoughtcrime.securesms.util; +import android.graphics.Typeface; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.StyleSpan; import android.widget.EditText; import java.util.concurrent.ExecutorService; @@ -90,6 +94,24 @@ public class Util { return value == null || value.getText() == null || isEmpty(value.getText().toString()); } + public static CharSequence getBoldedString(String value) { + SpannableString spanned = new SpannableString(value); + spanned.setSpan(new StyleSpan(Typeface.BOLD), 0, + spanned.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return spanned; + } + + public static CharSequence getItalicizedString(String value) { + SpannableString spanned = new SpannableString(value); + spanned.setSpan(new StyleSpan(Typeface.ITALIC), 0, + spanned.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return spanned; + } + // public static Bitmap loadScaledBitmap(InputStream src, int targetWidth, int targetHeight) { // return BitmapFactory.decodeStream(src); //// BitmapFactory.Options options = new BitmapFactory.Options();