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