Include incoming message body in notifications.

1) Refactor the master secret reset logic to properly interact with
   services.

2) Add support for "BigText" and "Inbox" style notifications.

3) Decrypt message bodies when unlocked, display 'encrypted' when
   locked.
master
Moxie Marlinspike 2013-02-08 11:57:54 -08:00
parent 10865bc75f
commit 0a8c62e0e3
14 changed files with 597 additions and 310 deletions

View File

@ -208,7 +208,7 @@
<string name="MessageDisplayHelper_decrypting_please_wait">Decrypting, please wait...</string>
<string name="MessageDisplayHelper_message_encrypted_for_non_existing_session">Message encrypted for non-existing session...</string>
<string name="MessageDisplayHelper_decryption_error_local_message_corrupted_mac_doesn_t_match_potential_tampering_question">Decryption error: local message corrupted, MAC doesn\'t match. Potential tampering?</string>
<!-- MmsDatabase -->
<string name="MmsDatabase_connecting_to_mms_server">Connecting to MMS server...</string>
<string name="MmsDatabase_downloading_mms">Downloading MMS...</string>
@ -230,9 +230,13 @@
<string name="KeyCachingService_passphrase_cached">Passphrase Cached</string>
<!-- MessageNotifier -->
<string name="MessageNotifier_d_new_messages">(%d) New messages</string>
<string name="MessageNotifier_d_new_messages_most_recent_from_s">(%1$d) New messages, most recent from: %2$s</string>
<string name="MessageNotifier_most_recent_from_s">Most recent from: %s</string>
<string name="MessageNotifier_d_new_messages">%d new messages</string>
<string name="MessageNotifier_most_recent_from_s">Most recent from: %s</string>
<string name="MessageNotifier_key_exchange">Key exchange...</string>
<string name="MessageNotifier_encrypted_message">Encrypted message...</string>
<string name="MessageNotifier_corrupted_ciphertext">Corrupted ciphertext</string>
<string name="MessageNotifier_no_subject">(No Subject)</string>
<!-- auto_initiate_activity -->
<string name="auto_initiate_activity__you_have_received_a_message_from_someone_who_supports_textsecure_encrypted_sessions_would_you_like_to_initiate_a_secure_session">You have received a message from someone who supports TextSecure encrypted sessions. Would you like to initiate a secure session?</string>

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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();

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<NotificationItem> 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<NotificationItem> 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(",");
}
}

View File

@ -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);
}
}

View File

@ -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<NotificationItem> notifications = new LinkedList<NotificationItem>();
private final Set<Long> threads = new HashSet<Long>();
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<NotificationItem> getNotifications() {
return notifications;
}
public Bitmap getContactPhoto() {
return notifications.get(0).getRecipients().getPrimaryRecipient().getContactPhoto();
}
}

View File

@ -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());

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Recipient>());
}
}
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(",");
}
}

View File

@ -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...");

View File

@ -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<Runnable> workQueue;
private List<Runnable> 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<Runnable> 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();
}
}
};
}

View File

@ -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);
}
}

View File

@ -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();