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.
This commit is contained in:
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

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