diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c57c8280b58afc10875034b3ec6b06122d528a0f..320e150803fd2c4bccfcc986922d134f09fcd1f1 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -54,10 +54,18 @@ android:resource="@xml/badge_widget_provider"/> + + + + + + + @@ -339,6 +347,21 @@ + + + + + + + + + + + + + - + android:layout_height="@dimen/media_bubble_height"/> + diff --git a/res/layout/conversation_item_sent.xml b/res/layout/conversation_item_sent.xml index bb7b07cc82d9b7ccda376cdd65eec6a3b983bff1..156d60fa7350d4d827e0907f853ff5163f12afb2 100644 --- a/res/layout/conversation_item_sent.xml +++ b/res/layout/conversation_item_sent.xml @@ -38,18 +38,11 @@ android:background="@drawable/sent_bubble" android:orientation="vertical"> - + android:layout="@layout/conversation_item_sent_thumbnail"/> + diff --git a/res/layout/view_identity_activity.xml b/res/layout/view_identity_activity.xml index 1bc52c3ac209fd0e84e8df9be8e539ac272a9a9a..71477d189c52f2f63beb7964921997a099b830b6 100644 --- a/res/layout/view_identity_activity.xml +++ b/res/layout/view_identity_activity.xml @@ -10,14 +10,13 @@ android:layout_gravity="center" android:orientation="vertical"> - + diff --git a/res/menu/conversation_insecure_no_push.xml b/res/menu/conversation_insecure_no_push.xml index 5c5b77c4dbc428ce2bdbee6dfeae5ab3e168c545..5a78c17d86ed412240bfdc626dd41f6a7fbab436 100644 --- a/res/menu/conversation_insecure_no_push.xml +++ b/res/menu/conversation_insecure_no_push.xml @@ -8,6 +8,10 @@ + + + diff --git a/res/menu/conversation_secure_identity.xml b/res/menu/conversation_secure_identity.xml index a1623b622ec3b5298688e0b8e9422ac33f25507b..75722bb965c7ec0fba8a306453bd9d7294ce7155 100644 --- a/res/menu/conversation_secure_identity.xml +++ b/res/menu/conversation_secure_identity.xml @@ -6,7 +6,12 @@ app:showAsAction="ifRoom"> + android:id="@+id/menu_verify_identity" /> + + + + diff --git a/res/menu/conversation_secure_sms.xml b/res/menu/conversation_secure_sms.xml index d30655c66af45412cc866fb902c57376573f35d4..ce947958c312e40ef126385e992a8e136d9473f4 100644 --- a/res/menu/conversation_secure_sms.xml +++ b/res/menu/conversation_secure_sms.xml @@ -1,5 +1,15 @@ - \ No newline at end of file + android:id="@+id/menu_abort_session" /> + + + + + + + + diff --git a/res/menu/text_secure_normal.xml b/res/menu/text_secure_normal.xml index 1114e9163dff428e2d91f1680719a154e744ad7e..44f72c1b5227514f45d5cd0dfa8c8229cde4488a 100644 --- a/res/menu/text_secure_normal.xml +++ b/res/menu/text_secure_normal.xml @@ -26,6 +26,12 @@ android:id="@+id/menu_my_identity" android:icon="@android:drawable/ic_menu_view" /> + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 63a9a46c31e21b2d46971e84a26e580559753736..c36b9533a33045ceb45a4d24135efcba0d5c67bd 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -212,6 +212,11 @@ Today Yesterday + + New SIM card detected + A new key has been generated. + A new key has been generated for that SIM card. + Share with @@ -357,6 +362,10 @@ Draft: Media message + + SMS messages disabled + No SIM card found + You do not have an identity key. Recipient has no identity key. @@ -439,6 +448,11 @@ Silence New message + + Slot %1$s + %1$s (slot %2$s) + + Old passphrase New passphrase diff --git a/res/xml/automotive_app_desc.xml b/res/xml/automotive_app_desc.xml new file mode 100644 index 0000000000000000000000000000000000000000..2603bffcbc2836d4ef24caced41a6c7e41c667e1 --- /dev/null +++ b/res/xml/automotive_app_desc.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/org/smssecure/smssecure/ApplicationContext.java b/src/org/smssecure/smssecure/ApplicationContext.java index c18c1d80eada2a8e6a525bf3bcab06959cce58a4..5eb4786a3824de30ed33a51f37a5ce4987cf80cd 100644 --- a/src/org/smssecure/smssecure/ApplicationContext.java +++ b/src/org/smssecure/smssecure/ApplicationContext.java @@ -18,6 +18,7 @@ package org.smssecure.smssecure; import android.app.Application; import android.content.Context; +import android.util.Log; import org.smssecure.smssecure.crypto.PRNGFixes; import org.smssecure.smssecure.dependencies.InjectableType; @@ -26,6 +27,7 @@ import org.smssecure.smssecure.jobs.requirements.MasterSecretRequirementProvider import org.smssecure.smssecure.jobs.requirements.MediaNetworkRequirementProvider; import org.smssecure.smssecure.jobs.requirements.ServiceRequirementProvider; import org.smssecure.smssecure.util.SilencePreferences; +import org.smssecure.smssecure.util.dualsim.SimChangedReceiver; import org.whispersystems.jobqueue.JobManager; import org.whispersystems.jobqueue.dependencies.DependencyInjector; import org.whispersystems.jobqueue.requirements.NetworkRequirementProvider; @@ -45,6 +47,7 @@ import dagger.ObjectGraph; * @author Moxie Marlinspike */ public class ApplicationContext extends Application implements DependencyInjector { + private static final String TAG = ApplicationContext.class.getSimpleName(); private JobManager jobManager; private ObjectGraph objectGraph; @@ -61,6 +64,7 @@ public class ApplicationContext extends Application implements DependencyInjecto initializeRandomNumberFix(); initializeLogging(); initializeJobManager(); + checkSimState(); } @Override @@ -99,4 +103,8 @@ public class ApplicationContext extends Application implements DependencyInjecto mediaNetworkRequirementProvider.notifyMediaControlEvent(); } + private void checkSimState() { + SimChangedReceiver.checkSimState(this); + } + } diff --git a/src/org/smssecure/smssecure/ConversationActivity.java b/src/org/smssecure/smssecure/ConversationActivity.java index 88d7984279053b7a40f0e474bc4fea2ff6018d83..09afd50271f21c6431311498ed559b7b661fdaf4 100644 --- a/src/org/smssecure/smssecure/ConversationActivity.java +++ b/src/org/smssecure/smssecure/ConversationActivity.java @@ -47,6 +47,7 @@ import android.util.Pair; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.SubMenu; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; @@ -105,7 +106,6 @@ import org.smssecure.smssecure.recipients.Recipients.RecipientsModifiedListener; import org.smssecure.smssecure.service.KeyCachingService; import org.smssecure.smssecure.sms.MessageSender; import org.smssecure.smssecure.sms.OutgoingEncryptedMessage; -import org.smssecure.smssecure.sms.OutgoingEndSessionMessage; import org.smssecure.smssecure.sms.OutgoingTextMessage; import org.smssecure.smssecure.util.concurrent.AssertedSuccessListener; import org.smssecure.smssecure.util.CharacterCalculator.CharacterState; @@ -120,6 +120,7 @@ import org.smssecure.smssecure.util.Util; import org.smssecure.smssecure.util.ViewUtil; import org.smssecure.smssecure.util.concurrent.ListenableFuture; import org.smssecure.smssecure.util.concurrent.SettableFuture; +import org.smssecure.smssecure.util.dualsim.SubscriptionInfoCompat; import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; @@ -193,6 +194,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private DynamicTheme dynamicTheme = new DynamicTheme(); private DynamicLanguage dynamicLanguage = new DynamicLanguage(); + private List activeSubscriptions; + @Override protected void onPreCreate() { dynamicTheme.onCreate(this); @@ -203,6 +206,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) { Log.w(TAG, "onCreate()"); this.masterSecret = masterSecret; + this.activeSubscriptions = SubscriptionManagerCompat.from(this).getActiveSubscriptionInfoList(); supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); setContentView(R.layout.conversation_activity); @@ -347,14 +351,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity MenuInflater inflater = this.getMenuInflater(); menu.clear(); + boolean isEncryptedForAllSubscriptionIdsConversation = SessionUtil.hasSession(this, masterSecret, recipients.getPrimaryRecipient().getNumber(), activeSubscriptions); + if (isSingleConversation() && isEncryptedConversation) { inflater.inflate(R.menu.conversation_secure_identity, menu); + inflateSubMenuVerifyIdentity(menu); inflater.inflate(R.menu.conversation_secure_sms, menu.findItem(R.id.menu_security).getSubMenu()); - } else if (isSingleConversation()) { + inflateSubMenuAbortSecureSession(menu); + } else if (isSingleConversation() && !isEncryptedConversation) { inflater.inflate(R.menu.conversation_insecure_no_push, menu); inflater.inflate(R.menu.conversation_insecure, menu); } + if (isSingleConversation() && !isEncryptedForAllSubscriptionIdsConversation) { + inflateSubMenuStartSecureSession(menu); + } else { + MenuItem item = menu.findItem(R.id.menu_start_secure_session); + if (item != null) item.setVisible(false); + } + if (isSingleConversation()) { inflater.inflate(R.menu.conversation_callable, menu); } else if (isGroupConversation()) { @@ -390,23 +405,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item); switch (item.getItemId()) { - case R.id.menu_call: handleDial(getRecipients().getPrimaryRecipient()); return true; - case R.id.menu_delete_conversation: handleDeleteConversation(); return true; - case R.id.menu_archive_conversation: handleArchiveConversation(); return true; - case R.id.menu_add_attachment: handleAddAttachment(); return true; - case R.id.menu_view_media: handleViewMedia(); return true; - case R.id.menu_add_to_contacts: handleAddToContacts(); return true; - case R.id.menu_start_secure_session: handleStartSecureSession(); return true; - case R.id.menu_abort_session: handleAbortSecureSession(); return true; - case R.id.menu_verify_identity: handleVerifyIdentity(); return true; - case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true; - case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true; - case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true; - case R.id.menu_invite: handleInviteLink(); return true; - case R.id.menu_mute_notifications: handleMuteNotifications(); return true; - case R.id.menu_unmute_notifications: handleUnmuteNotifications(); return true; - case R.id.menu_conversation_settings: handleConversationSettings(); return true; - case android.R.id.home: handleReturnToConversationList(); return true; + case R.id.menu_call: handleDial(getRecipients().getPrimaryRecipient()); return true; + case R.id.menu_delete_conversation: handleDeleteConversation(); return true; + case R.id.menu_archive_conversation: handleArchiveConversation(); return true; + case R.id.menu_add_attachment: handleAddAttachment(); return true; + case R.id.menu_view_media: handleViewMedia(); return true; + case R.id.menu_add_to_contacts: handleAddToContacts(); return true; + case R.id.menu_start_secure_session: handleStartSecureSession(); return true; + case R.id.menu_start_secure_session_dual_sim: handleStartSecureSession(); return true; + case R.id.menu_abort_session: handleAbortSecureSession(); return true; + case R.id.menu_abort_session_dual_sim: handleAbortSecureSession(); return true; + case R.id.menu_verify_identity: handleVerifyIdentity(); return true; + case R.id.menu_verify_identity_dual_sim: handleVerifyIdentity(); return true; + case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true; + case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true; + case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true; + case R.id.menu_invite: handleInviteLink(); return true; + case R.id.menu_mute_notifications: handleMuteNotifications(); return true; + case R.id.menu_unmute_notifications: handleUnmuteNotifications(); return true; + case R.id.menu_conversation_settings: handleConversationSettings(); return true; + case android.R.id.home: handleReturnToConversationList(); return true; } return false; @@ -424,6 +442,79 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity emojiToggle.setToEmoji(); } + private void inflateSubMenuVerifyIdentity(Menu menu) { + if (Build.VERSION.SDK_INT >= 22 && activeSubscriptions.size() > 1) { + menu.findItem(R.id.menu_verify_identity).setVisible(false); + SubMenu identitiesMenu = menu.findItem(R.id.menu_verify_identity_dual_sim).getSubMenu(); + + for (SubscriptionInfoCompat subscriptionInfo : activeSubscriptions) { + final int subscriptionId = subscriptionInfo.getSubscriptionId(); + identitiesMenu.add(Menu.NONE, Menu.NONE, Menu.NONE, subscriptionInfo.getDisplayName()) + .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + handleVerifyIdentity(subscriptionId); + return true; + } + }); + } + } else { + menu.findItem(R.id.menu_verify_identity_dual_sim).setVisible(false); + } + } + + private void inflateSubMenuStartSecureSession(Menu menu) { + if (Build.VERSION.SDK_INT >= 22 && activeSubscriptions.size() > 1) { + menu.findItem(R.id.menu_start_secure_session).setVisible(false); + SubMenu startSecureSessionMenu = menu.findItem(R.id.menu_start_secure_session_dual_sim).getSubMenu(); + + for (SubscriptionInfoCompat subscriptionInfo : activeSubscriptions) { + final int subscriptionId = subscriptionInfo.getSubscriptionId(); + + if (!SessionUtil.hasSession(this, masterSecret, recipients.getPrimaryRecipient().getNumber(), subscriptionId)) { + + startSecureSessionMenu.add(Menu.NONE, Menu.NONE, Menu.NONE, subscriptionInfo.getDisplayName()) + .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + handleStartSecureSession(subscriptionId); + return true; + } + }); + } + } + } else { + menu.findItem(R.id.menu_start_secure_session_dual_sim).setVisible(false); + } + } + + private void inflateSubMenuAbortSecureSession(Menu menu) { + if (Build.VERSION.SDK_INT >= 22 && activeSubscriptions.size() > 1) { + menu.findItem(R.id.menu_abort_session).setVisible(false); + SubMenu abortSecureSessionMenu = menu.findItem(R.id.menu_abort_session_dual_sim).getSubMenu(); + + for (SubscriptionInfoCompat subscriptionInfo : activeSubscriptions) { + final int subscriptionId = subscriptionInfo.getSubscriptionId(); + Log.w(TAG, "inflateSubMenuAbortSecureSession( " + subscriptionId + " )"); + + if (SessionUtil.hasSession(this, masterSecret, recipients.getPrimaryRecipient().getNumber(), subscriptionId)) { + Log.w(TAG, "Subscription ID " + subscriptionId + " has a secure session."); + + abortSecureSessionMenu.add(Menu.NONE, Menu.NONE, Menu.NONE, subscriptionInfo.getDisplayName()) + .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + handleAbortSecureSession(subscriptionId); + return true; + } + }); + } + } + } else { + menu.findItem(R.id.menu_abort_session).setVisible(false); + } + } + //////// Event Handlers private void handleReturnToConversationList() { @@ -497,12 +588,27 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void handleVerifyIdentity() { + if (activeSubscriptions.size() < 2) { + int subscriptionId = activeSubscriptions.get(0).getSubscriptionId(); + handleVerifyIdentity(subscriptionId); + } + } + + private void handleVerifyIdentity(int subscriptionId) { Intent verifyIdentityIntent = new Intent(this, VerifyIdentityActivity.class); + verifyIdentityIntent.putExtra("subscription_id", subscriptionId); verifyIdentityIntent.putExtra("recipient", getRecipients().getPrimaryRecipient().getRecipientId()); startActivity(verifyIdentityIntent); } private void handleStartSecureSession() { + if (activeSubscriptions.size() < 2) { + int subscriptionId = activeSubscriptions.get(0).getSubscriptionId(); + handleStartSecureSession(subscriptionId); + } + } + + private void handleStartSecureSession(final int subscriptionId) { if (getRecipients() == null) { Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient), Toast.LENGTH_LONG).show(); @@ -517,7 +623,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity final Recipients recipients = getRecipients(); final Recipient recipient = recipients.getPrimaryRecipient(); - final int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1); String recipientName = (recipient.getName() == null ? recipient.getNumber() : recipient.getName()); AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -529,9 +634,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - if (!isEncryptedConversation){ - KeyExchangeInitiator.initiate(ConversationActivity.this, masterSecret, recipients, true, subscriptionId); - } + KeyExchangeInitiator.initiate(ConversationActivity.this, masterSecret, recipients, true, subscriptionId); long allocatedThreadId; if (threadId == -1) { allocatedThreadId = DatabaseFactory.getThreadDatabase(getApplicationContext()).getThreadIdFor(recipients); @@ -548,6 +651,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void handleAbortSecureSession() { + if (activeSubscriptions.size() < 2) { + int subscriptionId = activeSubscriptions.get(0).getSubscriptionId(); + handleAbortSecureSession(subscriptionId); + } + } + + private void handleAbortSecureSession(final int subscriptionId) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.ConversationActivity_abort_secure_session_confirmation); builder.setIconAttribute(R.attr.dialog_alert_icon); @@ -556,23 +666,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - if (isSingleConversation() && isEncryptedConversation) { - final Context context = getApplicationContext(); - - OutgoingEndSessionMessage endSessionMessage = - new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE", -1)); - - new AsyncTask() { - @Override - protected Long doInBackground(OutgoingEndSessionMessage... messages) { - return MessageSender.send(context, masterSecret, messages[0], threadId, false); - } - - @Override - protected void onPostExecute(Long result) { - sendComplete(result); - } - }.execute(endSessionMessage); + if (isSingleConversation()) { + Recipients recipients = getRecipients(); + KeyExchangeInitiator.abort(ConversationActivity.this, masterSecret, recipients, subscriptionId); + + long allocatedThreadId; + if (threadId == -1) { + allocatedThreadId = DatabaseFactory.getThreadDatabase(getApplicationContext()).getThreadIdFor(recipients); + } else { + allocatedThreadId = threadId; + } + Log.w(TAG, "Refreshing thread "+allocatedThreadId+"..."); + sendComplete(allocatedThreadId); } } }); @@ -757,7 +862,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient(); boolean isMediaMessage = !recipients.isSingleRecipient() || attachmentManager.isAttachmentPresent(); - isSecureSmsDestination = isSingleConversation() && SessionUtil.hasSession(this, masterSecret, primaryRecipient); + isSecureSmsDestination = isSingleConversation() && SessionUtil.hasAtLeastOneSession(this, masterSecret, primaryRecipient.getNumber(), activeSubscriptions); if (isSecureSmsDestination) { this.isEncryptedConversation = true; @@ -769,6 +874,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (!isSecureSmsDestination ) sendButton.disableTransport(Type.SECURE_SMS); if (recipients.isGroupRecipient()) sendButton.disableTransport(Type.INSECURE_SMS); + if (Build.VERSION.SDK_INT >= 22) { + sendButton.disableTransport(Type.SECURE_SMS, SessionUtil.getSubscriptionIdWithoutSession(this, masterSecret, primaryRecipient.getNumber(), activeSubscriptions)); + } + if (isSecureSmsDestination) { sendButton.setDefaultTransport(Type.SECURE_SMS); } else { @@ -1184,6 +1293,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void sendMessage() { + TransportOption transportOption = sendButton.getSelectedTransport(); + + if (transportOption == null || transportOption.getType() == Type.DISABLED) return; + try { Recipients recipients = getRecipients(); diff --git a/src/org/smssecure/smssecure/ConversationItem.java b/src/org/smssecure/smssecure/ConversationItem.java index 03a6524d2902ee12a07097e6839b4a332d31dc7a..fcdb0f449e40a54fda6c58043500703715d73c93 100644 --- a/src/org/smssecure/smssecure/ConversationItem.java +++ b/src/org/smssecure/smssecure/ConversationItem.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PorterDuff; +import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; @@ -40,10 +41,10 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import org.smssecure.smssecure.components.AlertView; import org.smssecure.smssecure.components.AudioView; import org.smssecure.smssecure.components.AvatarImageView; import org.smssecure.smssecure.components.DeliveryStatusView; -import org.smssecure.smssecure.components.AlertView; import org.smssecure.smssecure.components.ThumbnailView; import org.smssecure.smssecure.crypto.KeyExchangeInitiator; import org.smssecure.smssecure.crypto.MasterSecret; @@ -66,16 +67,17 @@ import org.smssecure.smssecure.recipients.Recipient; import org.smssecure.smssecure.recipients.RecipientFactory; import org.smssecure.smssecure.recipients.Recipients; import org.smssecure.smssecure.util.DateUtils; +import org.smssecure.smssecure.util.DynamicTheme; +import org.smssecure.smssecure.util.SilencePreferences; +import org.smssecure.smssecure.util.Util; import org.smssecure.smssecure.util.dualsim.SubscriptionInfoCompat; import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; -import org.smssecure.smssecure.util.views.Stub; -import org.smssecure.smssecure.util.DynamicTheme; import org.smssecure.smssecure.util.TelephonyUtil; -import org.smssecure.smssecure.util.Util; -import org.smssecure.smssecure.util.SilencePreferences; +import org.smssecure.smssecure.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; @@ -113,7 +115,7 @@ public class ConversationItem extends LinearLayout private @NonNull Set batchSelected = new HashSet<>(); private @Nullable Recipients conversationRecipients; - private @NonNull ThumbnailView mediaThumbnail; + private @NonNull Stub mediaThumbnailStub; private @NonNull Stub audioViewStub; private @NonNull Button mmsDownloadButton; private @NonNull TextView mmsDownloadingLabel; @@ -179,17 +181,12 @@ public class ConversationItem extends LinearLayout this.mmsDownloadingLabel = (TextView) findViewById(R.id.mms_label_downloading); this.contactPhoto = (AvatarImageView) findViewById(R.id.contact_photo); this.bodyBubble = findViewById(R.id.body_bubble); - this.mediaThumbnail = (ThumbnailView) findViewById(R.id.image_view); + this.mediaThumbnailStub = new Stub<>((ViewStub) findViewById(R.id.image_view_stub)); this.audioViewStub = new Stub<>((ViewStub) findViewById(R.id.audio_view_stub)); - setOnClickListener(new ClickListener(null)); mmsDownloadButton.setOnClickListener(mmsDownloadClickListener); - mediaThumbnail.setThumbnailClickListener(new ThumbnailClickListener()); - mediaThumbnail.setDownloadClickListener(downloadClickListener); - mediaThumbnail.setOnLongClickListener(passthroughClickListener); - mediaThumbnail.setOnClickListener(passthroughClickListener); bodyText.setOnLongClickListener(passthroughClickListener); bodyText.setOnClickListener(passthroughClickListener); } @@ -250,14 +247,13 @@ public class ConversationItem extends LinearLayout private void setBubbleState(MessageRecord messageRecord, Recipient recipient) { if (messageRecord.isOutgoing()) { bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY); - mediaThumbnail.setBackgroundColorHint(defaultBubbleColor); + if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setBackgroundColorHint(defaultBubbleColor); } else { int color = recipient.getColor().toConversationColor(context); bodyBubble.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY); - mediaThumbnail.setBackgroundColorHint(color); + if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setBackgroundColorHint(color); } - if (audioViewStub.resolved()) { setAudioViewTint(messageRecord, conversationRecipients); } @@ -277,9 +273,12 @@ public class ConversationItem extends LinearLayout private void setInteractionState(MessageRecord messageRecord) { setSelected(batchSelected.contains(messageRecord)); - mediaThumbnail.setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); - mediaThumbnail.setClickable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); - mediaThumbnail.setLongClickable(batchSelected.isEmpty()); + + if (mediaThumbnailStub.resolved()) { + mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); + mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); + mediaThumbnailStub.get().setLongClickable(batchSelected.isEmpty()); + } if (audioViewStub.resolved()) { audioViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); @@ -339,14 +338,14 @@ public class ConversationItem extends LinearLayout boolean showControls = !messageRecord.isFailed() && (!messageRecord.isOutgoing() || messageRecord.isPending()); if (messageRecord.isMmsNotification()) { - mediaThumbnail.setVisibility(View.GONE); + if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); setNotificationMmsAttributes((NotificationMmsMessageRecord) messageRecord); } else if (hasAudio(messageRecord)) { audioViewStub.get().setVisibility(View.VISIBLE); - mediaThumbnail.setVisibility(View.GONE); + if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); //noinspection ConstantConditions audioViewStub.get().setAudio(masterSecret, ((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls); @@ -355,17 +354,22 @@ public class ConversationItem extends LinearLayout bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); } else if (hasThumbnail(messageRecord)) { - mediaThumbnail.setVisibility(View.VISIBLE); + mediaThumbnailStub.get().setVisibility(View.VISIBLE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); //noinspection ConstantConditions - mediaThumbnail.setImageResource(masterSecret, - ((MediaMmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide(), - showControls); + mediaThumbnailStub.get().setImageResource(masterSecret, + ((MediaMmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide(), + showControls); + mediaThumbnailStub.get().setThumbnailClickListener(new ThumbnailClickListener()); + mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener); + mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener); + mediaThumbnailStub.get().setOnClickListener(passthroughClickListener); + bodyText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } else { - mediaThumbnail.setVisibility(View.GONE); - if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); + if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); + if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } } @@ -398,9 +402,9 @@ public class ConversationItem extends LinearLayout } private void setSimInfo(MessageRecord messageRecord) { - SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(context); + SubscriptionManagerCompat subscriptionManager = SubscriptionManagerCompat.from(context); - if (messageRecord.getSubscriptionId() == -1 || subscriptionManager.getActiveSubscriptionInfoList().size() < 2) { + if (subscriptionManager.getActiveSubscriptionInfoList().size() < 2) { simInfoText.setVisibility(View.GONE); } else { Optional subscriptionInfo = subscriptionManager.getActiveSubscriptionInfo(messageRecord.getSubscriptionId()); @@ -517,7 +521,7 @@ public class ConversationItem extends LinearLayout builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - KeyExchangeInitiator.initiate(context, masterSecret, recipients, true, subscriptionId); + KeyExchangeInitiator.initiate(context, masterSecret, recipients, true); } }); builder.show(); @@ -557,15 +561,28 @@ public class ConversationItem extends LinearLayout } @Override - public void onModified(Recipients recipient) { - onModified(recipient.getPrimaryRecipient()); + public void onModified(final Recipients recipients) { + Util.runOnMain(new Runnable() { + @Override + public void run() { + setAudioViewTint(messageRecord, recipients); + } + }); } private class AttachmentDownloadClickListener implements SlideClickListener { - @Override public void onClick(View v, final Slide slide) { - DatabaseFactory.getAttachmentDatabase(context).setTransferState(messageRecord.getId(), - slide.asAttachment(), - AttachmentDatabase.TRANSFER_PROGRESS_STARTED); + @Override + public void onClick(View v, final Slide slide) { + if (messageRecord.isMmsNotification()) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new MmsDownloadJob(context, messageRecord.getId(), + messageRecord.getThreadId(), false)); + } else { + DatabaseFactory.getAttachmentDatabase(context).setTransferState(messageRecord.getId(), + slide.asAttachment(), + AttachmentDatabase.TRANSFER_PROGRESS_STARTED); + } } } @@ -596,7 +613,7 @@ public class ConversationItem extends LinearLayout intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, messageRecord.getThreadId()); context.startActivity(intent); - } else { + } else if (slide.getUri() != null) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.ConversationItem_view_secure_media_question); builder.setIconAttribute(R.attr.dialog_alert_icon); diff --git a/src/org/smssecure/smssecure/ConversationListActivity.java b/src/org/smssecure/smssecure/ConversationListActivity.java index 12d289a1a2c765a7502ade2015ace24f5f4ffc94..d18b077faa94a8c6767abcc217eae4ca2c5e4043 100644 --- a/src/org/smssecure/smssecure/ConversationListActivity.java +++ b/src/org/smssecure/smssecure/ConversationListActivity.java @@ -19,16 +19,18 @@ package org.smssecure.smssecure; import android.content.Intent; import android.database.ContentObserver; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.provider.ContactsContract; import android.support.annotation.NonNull; import android.support.v7.app.ActionBar; -import android.util.Log; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.SubMenu; import org.smssecure.smssecure.crypto.MasterSecret; import org.smssecure.smssecure.database.DatabaseFactory; @@ -36,10 +38,14 @@ import org.smssecure.smssecure.notifications.MessageNotifier; import org.smssecure.smssecure.recipients.RecipientFactory; import org.smssecure.smssecure.recipients.Recipients; import org.smssecure.smssecure.service.KeyCachingService; +import org.smssecure.smssecure.util.dualsim.SubscriptionInfoCompat; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; import org.smssecure.smssecure.util.DynamicLanguage; import org.smssecure.smssecure.util.DynamicTheme; import org.smssecure.smssecure.util.SilencePreferences; +import java.util.List; + public class ConversationListActivity extends PassphraseRequiredActionBarActivity implements ConversationListFragment.ConversationSelectedListener { @@ -52,6 +58,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit private ContentObserver observer; private MasterSecret masterSecret; + private List activeSubscriptions; + @Override protected void onPreCreate() { dynamicTheme.onCreate(this); @@ -61,6 +69,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit @Override protected void onCreate(Bundle icicle, @NonNull MasterSecret masterSecret) { this.masterSecret = masterSecret; + this.activeSubscriptions = SubscriptionManagerCompat.from(this).getActiveSubscriptionInfoList(); getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE); getSupportActionBar().setTitle(R.string.app_name); @@ -91,6 +100,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit menu.findItem(R.id.menu_clear_passphrase).setVisible(!SilencePreferences.isPasswordDisabled(this)); + inflateViewIdentities(menu); + inflater.inflate(R.menu.conversation_list, menu); MenuItem menuItem = menu.findItem(R.id.menu_search); initializeSearch(menuItem); @@ -99,6 +110,27 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit return true; } + private void inflateViewIdentities(Menu menu) { + if (Build.VERSION.SDK_INT >= 22 && activeSubscriptions.size() > 1) { + menu.findItem(R.id.menu_my_identity).setVisible(false); + MenuItem menuItem = menu.findItem(R.id.menu_my_identity_dual_sim); + SubMenu identitiesMenu = menuItem.getSubMenu(); + for (SubscriptionInfoCompat subscriptionInfo : activeSubscriptions) { + final int subscriptionId = subscriptionInfo.getSubscriptionId(); + identitiesMenu.add(Menu.NONE, Menu.NONE, Menu.NONE, subscriptionInfo.getDisplayName()) + .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + handleMyIdentity(subscriptionId); + return true; + } + }); + } + } else { + menu.findItem(R.id.menu_my_identity_dual_sim).setVisible(false); + } + } + private void initializeSearch(MenuItem searchViewItem) { SearchView searchView = (SearchView)MenuItemCompat.getActionView(searchViewItem); searchView.setQueryHint(getString(R.string.ConversationListActivity_search)); @@ -197,7 +229,16 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit } private void handleMyIdentity() { - startActivity(new Intent(this, ViewLocalIdentityActivity.class)); + if (activeSubscriptions.size() < 2) { + int subscriptionId = activeSubscriptions.get(0).getSubscriptionId(); + handleMyIdentity(subscriptionId); + } + } + + private void handleMyIdentity(int subscriptionId) { + Intent intent = new Intent(this, ViewIdentityActivity.class); + intent.putExtra("subscription_id", subscriptionId); + startActivity(intent); } private void handleMarkAllRead() { diff --git a/src/org/smssecure/smssecure/ConversationListFragment.java b/src/org/smssecure/smssecure/ConversationListFragment.java index b21b20e88f101412c78cb96b811887c4520601f6..27cd77565afb13ab7945202c3768e8dfcedeca90 100644 --- a/src/org/smssecure/smssecure/ConversationListFragment.java +++ b/src/org/smssecure/smssecure/ConversationListFragment.java @@ -78,6 +78,7 @@ import org.smssecure.smssecure.recipients.Recipients; import org.smssecure.smssecure.sms.MessageSender; import org.smssecure.smssecure.sms.OutgoingEncryptedMessage; import org.smssecure.smssecure.sms.OutgoingTextMessage; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; import org.smssecure.smssecure.util.Util; import org.smssecure.smssecure.util.ViewUtil; import org.smssecure.smssecure.util.task.SnackbarAsyncTask; @@ -348,8 +349,9 @@ public class ConversationListFragment extends Fragment recipients = getListAdapter().getRecipientsFromThreadId(threadId); if (recipients != null) { + int subscriptionId = SubscriptionManagerCompat.getDefaultMessagingSubscriptionId().or(-1); isSingleConversation = recipients.isSingleRecipient() && !recipients.isGroupRecipient(); - isSecureDestination = isSingleConversation && SessionUtil.hasSession(context, masterSecret, recipients.getPrimaryRecipient()); + isSecureDestination = isSingleConversation && SessionUtil.hasSession(context, masterSecret, recipients.getPrimaryRecipient().getNumber(), subscriptionId); Log.w(TAG, "Number of drafts: " + drafts.size()); if (drafts.size() > 1 && !drafts.get(1).getType().equals(DraftDatabase.Draft.TEXT)) { diff --git a/src/org/smssecure/smssecure/DatabaseUpgradeActivity.java b/src/org/smssecure/smssecure/DatabaseUpgradeActivity.java index bc5fed0bd073b8baca502acd3d1ad58cf05cf52d..3a31a1433b6f4cf223a385bf35e1c1f121c4cce0 100644 --- a/src/org/smssecure/smssecure/DatabaseUpgradeActivity.java +++ b/src/org/smssecure/smssecure/DatabaseUpgradeActivity.java @@ -21,28 +21,26 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; import android.util.Log; import android.view.View; import android.widget.ProgressBar; -import org.smssecure.smssecure.crypto.IdentityKeyUtil; import org.smssecure.smssecure.crypto.MasterSecret; import org.smssecure.smssecure.database.DatabaseFactory; -import org.smssecure.smssecure.database.MmsDatabase; -import org.smssecure.smssecure.database.MmsDatabase.Reader; -import org.smssecure.smssecure.database.EncryptingSmsDatabase; -import org.smssecure.smssecure.database.model.MessageRecord; -import org.smssecure.smssecure.database.SmsDatabase; -import org.smssecure.smssecure.database.model.SmsMessageRecord; -import org.smssecure.smssecure.jobs.SmsDecryptJob; import org.smssecure.smssecure.notifications.MessageNotifier; +import org.smssecure.smssecure.util.dualsim.DualSimUtil; +import org.smssecure.smssecure.util.dualsim.SubscriptionInfoCompat; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; import org.smssecure.smssecure.util.ParcelUtil; import org.smssecure.smssecure.util.Util; import org.smssecure.smssecure.util.VersionTracker; +import org.smssecure.smssecure.util.SilencePreferences; import org.whispersystems.jobqueue.EncryptionKeys; -import java.io.File; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; @@ -50,23 +48,10 @@ import java.util.TreeSet; public class DatabaseUpgradeActivity extends BaseActivity { private static final String TAG = DatabaseUpgradeActivity.class.getSimpleName(); - public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 46; - public static final int MMS_BODY_VERSION = 46; - public static final int TOFU_IDENTITIES_VERSION = 50; - public static final int CURVE25519_VERSION = 63; - public static final int ASYMMETRIC_MASTER_SECRET_FIX_VERSION = 73; - public static final int NO_V1_VERSION = 83; - public static final int SIGNED_PREKEY_VERSION = 83; - public static final int NO_DECRYPT_QUEUE_VERSION = 84; + public static final int MULTI_SIM_MULTI_KEYS_VERSION = 200; private static final SortedSet UPGRADE_VERSIONS = new TreeSet() {{ - add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION); - add(TOFU_IDENTITIES_VERSION); - add(CURVE25519_VERSION); - add(ASYMMETRIC_MASTER_SECRET_FIX_VERSION); - add(NO_V1_VERSION); - add(SIGNED_PREKEY_VERSION); - add(NO_DECRYPT_QUEUE_VERSION); + add(MULTI_SIM_MULTI_KEYS_VERSION); }}; private MasterSecret masterSecret; @@ -77,7 +62,7 @@ public class DatabaseUpgradeActivity extends BaseActivity { this.masterSecret = getIntent().getParcelableExtra("master_secret"); if (needsUpgradeTask()) { - Log.w("DatabaseUpgradeActivity", "Upgrading..."); + Log.w(TAG, "Upgrading..."); setContentView(R.layout.database_upgrade_activity); ProgressBar indeterminateProgress = (ProgressBar)findViewById(R.id.indeterminate_progress); @@ -101,13 +86,13 @@ public class DatabaseUpgradeActivity extends BaseActivity { int currentVersionCode = Util.getCurrentApkReleaseVersion(this); int lastSeenVersion = VersionTracker.getLastSeenVersion(this); - Log.w("DatabaseUpgradeActivity", "LastSeenVersion: " + lastSeenVersion); + Log.w(TAG, "LastSeenVersion: " + lastSeenVersion); if (lastSeenVersion >= currentVersionCode) return false; for (int version : UPGRADE_VERSIONS) { - Log.w("DatabaseUpgradeActivity", "Comparing: " + version); + Log.w(TAG, "Comparing: " + version); if (lastSeenVersion < version) return true; } @@ -156,50 +141,31 @@ public class DatabaseUpgradeActivity extends BaseActivity { protected Void doInBackground(Integer... params) { Context context = DatabaseUpgradeActivity.this.getApplicationContext(); - Log.w("DatabaseUpgradeActivity", "Running background upgrade.."); + Log.w(TAG, "Running background upgrade.."); DatabaseFactory.getInstance(DatabaseUpgradeActivity.this) .onApplicationLevelUpgrade(context, masterSecret, params[0], this); - if (params[0] < CURVE25519_VERSION) { - if (!IdentityKeyUtil.hasCurve25519IdentityKeys(context)) { - IdentityKeyUtil.generateCurve25519IdentityKeys(context, masterSecret); - } - } - - if (params[0] < NO_V1_VERSION) { - File v1sessions = new File(context.getFilesDir(), "sessions"); - - if (v1sessions.exists() && v1sessions.isDirectory()) { - File[] contents = v1sessions.listFiles(); - - if (contents != null) { - for (File session : contents) { - session.delete(); + if (params[0] < MULTI_SIM_MULTI_KEYS_VERSION) { + if (Build.VERSION.SDK_INT >= 22) { + /* + * getDefaultSubscriptionId() is available for API 24+ only, so we + * move keys and sessions to SIM card in the first available slot, + * not to the default one. + */ + List subscriptionInfoList = SubscriptionManagerCompat.from(context).getActiveSubscriptionInfoList(); + int smallerSlot = -1; + int eligibleDeviceSubscriptionId = -1; + + for (SubscriptionInfoCompat subscriptionInfo : subscriptionInfoList) { + if (smallerSlot == -1 || subscriptionInfo.getIccSlot() < smallerSlot) { + smallerSlot = subscriptionInfo.getIccSlot(); + eligibleDeviceSubscriptionId = subscriptionInfo.getDeviceSubscriptionId(); } } - v1sessions.delete(); - } - } - - if (params[0] < NO_DECRYPT_QUEUE_VERSION) { - EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(getApplicationContext()); - - SmsDatabase.Reader smsReader = null; - - SmsMessageRecord record; - - try { - smsReader = smsDatabase.getDecryptInProgressMessages(masterSecret); - - while ((record = smsReader.getNext()) != null) { - ApplicationContext.getInstance(getApplicationContext()) - .getJobManager() - .add(new SmsDecryptJob(getApplicationContext(), record.getId())); - } - } finally { - if (smsReader != null) - smsReader.close(); + DualSimUtil.moveIdentityKeysAndSessionsToSubscriptionId(context, -1, eligibleDeviceSubscriptionId); + DualSimUtil.generateKeysIfDoNotExist(context, masterSecret, subscriptionInfoList); + SubscriptionManagerCompat.from(context).updateActiveSubscriptionInfoList(); } } diff --git a/src/org/smssecure/smssecure/KeyScanningActivity.java b/src/org/smssecure/smssecure/KeyScanningActivity.java index bfcd9082ec7101adee3d06ea400fc562a5b8823f..07600c232dec0390825da775276fdf509e7a3f07 100644 --- a/src/org/smssecure/smssecure/KeyScanningActivity.java +++ b/src/org/smssecure/smssecure/KeyScanningActivity.java @@ -119,8 +119,14 @@ public abstract class KeyScanningActivity extends PassphraseRequiredActionBarAct } protected void initiateDisplay() { - IntentIntegrator intentIntegrator = getIntentIntegrator(); - intentIntegrator.shareText(Base64.encodeBytes(getIdentityKeyToDisplay().serialize())); + IdentityKey identityKey = getIdentityKeyToDisplay(); + if (identityKey != null) { + IntentIntegrator intentIntegrator = getIntentIntegrator(); + intentIntegrator.shareText(Base64.encodeBytes(identityKey.serialize())); + } else { + Toast.makeText(this, R.string.VerifyIdentityActivity_you_do_not_have_an_identity_key, + Toast.LENGTH_LONG).show(); + } } protected void initiateShare() { diff --git a/src/org/smssecure/smssecure/PassphraseCreateActivity.java b/src/org/smssecure/smssecure/PassphraseCreateActivity.java index f6235472ccccd50a2e8500622a7a6a6f8d81261e..7c0f756da9df437de4c472824c224269924655fe 100644 --- a/src/org/smssecure/smssecure/PassphraseCreateActivity.java +++ b/src/org/smssecure/smssecure/PassphraseCreateActivity.java @@ -17,15 +17,21 @@ package org.smssecure.smssecure; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.support.v7.app.ActionBar; import org.smssecure.smssecure.crypto.IdentityKeyUtil; import org.smssecure.smssecure.crypto.MasterSecret; import org.smssecure.smssecure.crypto.MasterSecretUtil; +import org.smssecure.smssecure.util.dualsim.DualSimUtil; +import org.smssecure.smssecure.util.dualsim.SubscriptionInfoCompat; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; import org.smssecure.smssecure.util.SilencePreferences; import org.smssecure.smssecure.util.VersionTracker; +import java.util.List; + /** * Activity for creating a user's local encryption passphrase. * @@ -53,7 +59,7 @@ public class PassphraseCreateActivity extends PassphraseActivity { } private class SecretGenerator extends AsyncTask { - private MasterSecret masterSecret; + private MasterSecret masterSecret; @Override protected void onPreExecute() { @@ -66,7 +72,16 @@ public class PassphraseCreateActivity extends PassphraseActivity { passphrase); MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret); - IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this, masterSecret); + + SubscriptionManagerCompat subscriptionManagerCompat = SubscriptionManagerCompat.from(PassphraseCreateActivity.this); + + if (Build.VERSION.SDK_INT >= 22) { + List activeSubscriptions = subscriptionManagerCompat.getActiveSubscriptionInfoList(); + DualSimUtil.generateKeysIfDoNotExist(PassphraseCreateActivity.this, masterSecret, activeSubscriptions, false); + } else { + IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this, masterSecret, -1, false); + subscriptionManagerCompat.updateActiveSubscriptionInfoList(); + } VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this); SilencePreferences.setPasswordDisabled(PassphraseCreateActivity.this, true); diff --git a/src/org/smssecure/smssecure/ReceiveKeyDialog.java b/src/org/smssecure/smssecure/ReceiveKeyDialog.java index 3f45af841b521a270aac84e8deb503b5d0950fac..cec56b428dac1a8287828c636b521ad787d8cb62 100644 --- a/src/org/smssecure/smssecure/ReceiveKeyDialog.java +++ b/src/org/smssecure/smssecure/ReceiveKeyDialog.java @@ -77,7 +77,7 @@ public class ReceiveKeyDialog extends AlertDialog { final IncomingKeyExchangeMessage message = getMessage(messageRecord); final IdentityKey identityKey = getIdentityKey(message); - if (isTrusted(masterSecret, identityKey, messageRecord.getIndividualRecipient())){ + if (isTrusted(masterSecret, identityKey, messageRecord.getIndividualRecipient(), messageRecord.getSubscriptionId())){ setMessage(context.getString(R.string.ReceiveKeyActivity_the_signature_on_this_key_exchange_is_trusted_but)); } else { setUntrustedText(messageRecord, identityKey); @@ -121,8 +121,8 @@ public class ReceiveKeyDialog extends AlertDialog { setMessage(spannableString); } - private boolean isTrusted(MasterSecret masterSecret, IdentityKey identityKey, Recipient recipient) { - IdentityKeyStore identityKeyStore = new SilenceIdentityKeyStore(getContext(), masterSecret); + private boolean isTrusted(MasterSecret masterSecret, IdentityKey identityKey, Recipient recipient, int subscriptionId) { + IdentityKeyStore identityKeyStore = new SilenceIdentityKeyStore(getContext(), masterSecret, subscriptionId); return identityKeyStore.isTrustedIdentity(new SignalProtocolAddress(recipient.getNumber(), 1), identityKey); } @@ -134,7 +134,8 @@ public class ReceiveKeyDialog extends AlertDialog { IncomingTextMessage message = new IncomingTextMessage(messageRecord.getIndividualRecipient().getNumber(), messageRecord.getRecipientDeviceId(), System.currentTimeMillis(), - messageRecord.getBody().getBody()); + messageRecord.getBody().getBody(), + messageRecord.getSubscriptionId()); if (messageRecord.isBundleKeyExchange()) { return new IncomingPreKeyBundleMessage(message, message.getMessageBody()); diff --git a/src/org/smssecure/smssecure/TransportOption.java b/src/org/smssecure/smssecure/TransportOption.java index cadbe05363e71b05511fd382170182e4e0485307..68a63470722f643a43b24cab447909baeda4cb54 100644 --- a/src/org/smssecure/smssecure/TransportOption.java +++ b/src/org/smssecure/smssecure/TransportOption.java @@ -12,6 +12,7 @@ import org.whispersystems.libsignal.util.guava.Optional; public class TransportOption { public enum Type { + DISABLED, INSECURE_SMS, SECURE_SMS } @@ -25,17 +26,6 @@ public class TransportOption { private final @NonNull Optional simName; private final @NonNull Optional simSubscriptionId; - public TransportOption(@NonNull Type type, - @DrawableRes int drawable, - int backgroundColor, - @NonNull String text, - @NonNull String composeHint, - @NonNull CharacterCalculator characterCalculator) - { - this(type, drawable, backgroundColor, text, composeHint, characterCalculator, - Optional.absent(), Optional.absent()); - } - public TransportOption(@NonNull Type type, @DrawableRes int drawable, int backgroundColor, diff --git a/src/org/smssecure/smssecure/TransportOptions.java b/src/org/smssecure/smssecure/TransportOptions.java index f6b3edd519318eca89da69aa9b59c8fbdcae3650..c51058c38fbae82d052cdcec2a76e73d7117b241 100644 --- a/src/org/smssecure/smssecure/TransportOptions.java +++ b/src/org/smssecure/smssecure/TransportOptions.java @@ -6,6 +6,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.smssecure.smssecure.util.CharacterCalculator; +import org.smssecure.smssecure.util.DummyCharacterCalculator; import org.smssecure.smssecure.util.MmsCharacterCalculator; import org.smssecure.smssecure.util.SmsCharacterCalculator; import org.smssecure.smssecure.util.EncryptedSmsCharacterCalculator; @@ -97,7 +98,7 @@ public class TransportOptions { } } - throw new AssertionError("No options of default type!"); + return getDefaultTransportOption(); } public void disableTransport(Type type) { @@ -111,6 +112,17 @@ public class TransportOptions { } } + public void disableTransport(Type type, int subscriptionId) { + List options = find(type); + + for (TransportOption option : options) { + if (option.getSimSubscriptionId().or(-1) == subscriptionId) enabledTransports.remove(option); + if (selectedOption.isPresent() && selectedOption.get().getType() == type && selectedOption.get().getSimSubscriptionId().or(-1) == subscriptionId) { + setSelectedTransport(null); + } + } + } + public List getEnabledTransports() { return enabledTransports; } @@ -157,27 +169,18 @@ public class TransportOptions { @NonNull CharacterCalculator characterCalculator) { List results = new LinkedList<>(); - SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(context); + SubscriptionManagerCompat subscriptionManager = SubscriptionManagerCompat.from(context); List subscriptions = subscriptionManager.getActiveSubscriptionInfoList(); - if (subscriptions.size() < 2) { + for (SubscriptionInfoCompat subscriptionInfo : subscriptions) { results.add(new TransportOption(type, drawable, backgroundColor, text, composeHint, - characterCalculator)); - } else { - for (SubscriptionInfoCompat subscriptionInfo : subscriptions) { - results.add(new TransportOption(type, - drawable, - backgroundColor, - text, - composeHint, - characterCalculator, - Optional.of(subscriptionInfo.getDisplayName()), - Optional.of(subscriptionInfo.getSubscriptionId()))); - } + characterCalculator, + Optional.of(subscriptionInfo.getDisplayName()), + Optional.of(subscriptionInfo.getSubscriptionId()))); } return results; @@ -210,4 +213,15 @@ public class TransportOptions { public interface OnTransportChangedListener { public void onChange(TransportOption newTransport, boolean manuallySelected); } + + private TransportOption getDefaultTransportOption() { + return new TransportOption(Type.DISABLED, + R.drawable.ic_send_insecure_white_24dp, + context.getResources().getColor(R.color.grey_600), + context.getString(R.string.TransportOptions_sms_disabled), + context.getString(R.string.TransportOptions_no_sim_card_found), + new DummyCharacterCalculator(), + Optional.of((CharSequence) ""), + Optional.of(-1)); + } } diff --git a/src/org/smssecure/smssecure/VerifyIdentityActivity.java b/src/org/smssecure/smssecure/VerifyIdentityActivity.java index ec915c16dae7a45e9fa380da6f54d95350ba3149..e87e24096251e7cbea2e99eadd7502f7021a47ba 100644 --- a/src/org/smssecure/smssecure/VerifyIdentityActivity.java +++ b/src/org/smssecure/smssecure/VerifyIdentityActivity.java @@ -29,6 +29,7 @@ import org.smssecure.smssecure.crypto.MasterSecret; import org.smssecure.smssecure.crypto.storage.SilenceSessionStore; import org.smssecure.smssecure.recipients.Recipient; import org.smssecure.smssecure.recipients.RecipientFactory; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; import org.smssecure.smssecure.util.Hex; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.IdentityKey; @@ -70,12 +71,14 @@ public class VerifyIdentityActivity extends KeyScanningActivity { } private void initializeFingerprints() { - if (!IdentityKeyUtil.hasIdentityKey(this)) { + int subscriptionId = getIntent().getIntExtra("subscription_id", SubscriptionManagerCompat.getDefaultMessagingSubscriptionId().or(-1)); + + if (!IdentityKeyUtil.hasIdentityKey(this, subscriptionId)) { localIdentityFingerprint.setText(R.string.VerifyIdentityActivity_you_do_not_have_an_identity_key); return; } - localIdentityFingerprint.setText(Hex.toString(IdentityKeyUtil.getIdentityKey(this).serialize())); + localIdentityFingerprint.setText(Hex.toString(IdentityKeyUtil.getIdentityKey(this, subscriptionId).serialize())); IdentityKey identityKey = getRemoteIdentityKey(masterSecret, recipient); @@ -88,7 +91,9 @@ public class VerifyIdentityActivity extends KeyScanningActivity { @Override protected void initiateDisplay() { - if (!IdentityKeyUtil.hasIdentityKey(this)) { + int subscriptionId = SubscriptionManagerCompat.getDefaultMessagingSubscriptionId().or(-1); + + if (!IdentityKeyUtil.hasIdentityKey(this, subscriptionId)) { Toast.makeText(this, R.string.VerifyIdentityActivity_you_do_not_have_an_identity_key, Toast.LENGTH_LONG).show(); @@ -127,7 +132,9 @@ public class VerifyIdentityActivity extends KeyScanningActivity { @Override protected IdentityKey getIdentityKeyToDisplay() { - return IdentityKeyUtil.getIdentityKey(this); + int subscriptionId = SubscriptionManagerCompat.getDefaultMessagingSubscriptionId().or(-1); + + return IdentityKeyUtil.getIdentityKey(this, subscriptionId); } @Override @@ -151,13 +158,14 @@ public class VerifyIdentityActivity extends KeyScanningActivity { } private @Nullable IdentityKey getRemoteIdentityKey(MasterSecret masterSecret, Recipient recipient) { + int subscriptionId = SubscriptionManagerCompat.getDefaultMessagingSubscriptionId().or(-1); IdentityKeyParcelable identityKeyParcelable = getIntent().getParcelableExtra("remote_identity"); if (identityKeyParcelable != null) { return identityKeyParcelable.get(); } - SessionStore sessionStore = new SilenceSessionStore(this, masterSecret); + SessionStore sessionStore = new SilenceSessionStore(this, masterSecret, subscriptionId); SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(recipient.getNumber(), 1); SessionRecord record = sessionStore.loadSession(axolotlAddress); diff --git a/src/org/smssecure/smssecure/ViewIdentityActivity.java b/src/org/smssecure/smssecure/ViewIdentityActivity.java index 18b3bc951335461c4c763664dfc2a4c71cf61b1c..5e671845510e8205b30328a240a1eebbfb0d6075 100644 --- a/src/org/smssecure/smssecure/ViewIdentityActivity.java +++ b/src/org/smssecure/smssecure/ViewIdentityActivity.java @@ -19,11 +19,17 @@ package org.smssecure.smssecure; import android.os.Bundle; import android.support.annotation.NonNull; import android.widget.TextView; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import org.smssecure.smssecure.crypto.IdentityKeyParcelable; +import org.smssecure.smssecure.crypto.IdentityKeyUtil; import org.smssecure.smssecure.crypto.MasterSecret; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; import org.smssecure.smssecure.util.Hex; + import org.whispersystems.libsignal.IdentityKey; -import org.smssecure.smssecure.crypto.IdentityKeyParcelable; /** * Activity for displaying an identity key. @@ -39,13 +45,41 @@ public class ViewIdentityActivity extends KeyScanningActivity { private IdentityKey identityKey; @Override - protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) { + protected void onCreate(Bundle icicle, @NonNull MasterSecret masterSecret) { + int subscriptionId = getIntent().getIntExtra("subscription_id", SubscriptionManagerCompat.getDefaultMessagingSubscriptionId().or(-1)); + + getIntent().putExtra(ViewIdentityActivity.IDENTITY_KEY, + new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this, subscriptionId))); + getIntent().putExtra(ViewIdentityActivity.TITLE, + getString(R.string.ViewIdentityActivity_your_identity_fingerprint)); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); setContentView(R.layout.view_identity_activity); initialize(); } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + MenuInflater inflater = this.getMenuInflater(); + inflater.inflate(R.menu.local_identity, menu); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + + switch (item.getItemId()) { + case android.R.id.home:finish(); return true; + } + + return false; + } + protected void initialize() { initializeResources(); initializeFingerprint(); diff --git a/src/org/smssecure/smssecure/ViewLocalIdentityActivity.java b/src/org/smssecure/smssecure/ViewLocalIdentityActivity.java deleted file mode 100644 index 6ecb1ccc9423346021d254e1b3e40ae6889901a7..0000000000000000000000000000000000000000 --- a/src/org/smssecure/smssecure/ViewLocalIdentityActivity.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * Copyright (C) 2013 Open 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.smssecure.smssecure; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import org.smssecure.smssecure.crypto.IdentityKeyUtil; -import org.smssecure.smssecure.crypto.IdentityKeyParcelable; -import org.smssecure.smssecure.crypto.MasterSecret; - -/** - * Activity that displays the local identity key and offers the option to regenerate it. - * - * @author Moxie Marlinspike - */ -public class ViewLocalIdentityActivity extends ViewIdentityActivity { - - @Override - protected void onCreate(Bundle icicle, @NonNull MasterSecret masterSecret) { - getIntent().putExtra(ViewIdentityActivity.IDENTITY_KEY, - new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this))); - getIntent().putExtra(ViewIdentityActivity.TITLE, - getString(R.string.ViewIdentityActivity_your_identity_fingerprint)); - super.onCreate(icicle, masterSecret); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - - MenuInflater inflater = this.getMenuInflater(); - inflater.inflate(R.menu.local_identity, menu); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - - switch (item.getItemId()) { - case android.R.id.home:finish(); return true; - } - - return false; - } -} diff --git a/src/org/smssecure/smssecure/components/AvatarImageView.java b/src/org/smssecure/smssecure/components/AvatarImageView.java index ad1b4595a2250107916a907472bf06fe4d74fc49..c2e9e0265ef9c2c56e2a386e0723d9aaea501322 100644 --- a/src/org/smssecure/smssecure/components/AvatarImageView.java +++ b/src/org/smssecure/smssecure/components/AvatarImageView.java @@ -24,6 +24,10 @@ import org.smssecure.smssecure.recipients.Recipient; import org.smssecure.smssecure.recipients.RecipientFactory; import org.smssecure.smssecure.recipients.Recipients; import org.smssecure.smssecure.service.KeyCachingService; +import org.smssecure.smssecure.util.dualsim.SubscriptionInfoCompat; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; + +import java.util.List; public class AvatarImageView extends ImageView { @@ -92,17 +96,19 @@ public class AvatarImageView extends ImageView { private class BadgeResolutionTask extends AsyncTask> { private final Context context; - private MasterSecret masterSecret; + private MasterSecret masterSecret; + private final List activeSubscriptions; public BadgeResolutionTask(Context context, MasterSecret masterSecret) { this.context = context; this.masterSecret = masterSecret; + this.activeSubscriptions = SubscriptionManagerCompat.from(context).getActiveSubscriptionInfoList(); } @Override protected Pair doInBackground(Recipients... recipients) { Boolean isSecureSmsDestination = masterSecret != null && - SessionUtil.hasSession(context, masterSecret, recipients[0].getPrimaryRecipient()); + SessionUtil.hasAtLeastOneSession(context, masterSecret, recipients[0].getPrimaryRecipient().getNumber(), activeSubscriptions); return new Pair<>(recipients[0], isSecureSmsDestination); } diff --git a/src/org/smssecure/smssecure/components/SendButton.java b/src/org/smssecure/smssecure/components/SendButton.java index 94e4b60c875f708b82f729862949a372928f03aa..b920259e88be83c99010584bed2811f981beaf40 100644 --- a/src/org/smssecure/smssecure/components/SendButton.java +++ b/src/org/smssecure/smssecure/components/SendButton.java @@ -12,6 +12,8 @@ import org.smssecure.smssecure.TransportOptionsPopup; import org.smssecure.smssecure.util.ViewUtil; import org.whispersystems.libsignal.util.guava.Optional; +import java.util.List; + public class SendButton extends ImageButton implements TransportOptions.OnTransportChangedListener, TransportOptionsPopup.SelectedListener, @@ -79,6 +81,16 @@ public class SendButton extends ImageButton transportOptions.disableTransport(type); } + public void disableTransport(TransportOption.Type type, int subscriptionId) { + transportOptions.disableTransport(type, subscriptionId); + } + + public void disableTransport(TransportOption.Type type, List subscriptionIds) { + for (int subscriptionId : subscriptionIds) { + transportOptions.disableTransport(type, subscriptionId); + } + } + public void setDefaultTransport(TransportOption.Type type) { transportOptions.setDefaultTransport(type); } diff --git a/src/org/smssecure/smssecure/crypto/IdentityKeyUtil.java b/src/org/smssecure/smssecure/crypto/IdentityKeyUtil.java index 2b69230d04b5bf3e21ec17263c74a880b4b12cc0..33a507b13bf9821b20e7d2781fbd54a79c10d096 100644 --- a/src/org/smssecure/smssecure/crypto/IdentityKeyUtil.java +++ b/src/org/smssecure/smssecure/crypto/IdentityKeyUtil.java @@ -1,4 +1,4 @@ -/** +/** * Copyright (C) 2011 Whisper Systems * Copyright (C) 2013 Open Whisper Systems * @@ -20,9 +20,11 @@ package org.smssecure.smssecure.crypto; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.os.Build; import android.util.Log; import org.smssecure.smssecure.util.Base64; +import org.smssecure.smssecure.util.dualsim.DualSimUtil; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -39,43 +41,45 @@ import java.io.IOException; */ public class IdentityKeyUtil { + private final static String TAG = IdentityKeyUtil.class.getSimpleName(); private static final String IDENTITY_PUBLIC_KEY_DJB_PREF = "pref_identity_public_curve25519"; private static final String IDENTITY_PRIVATE_KEY_DJB_PREF = "pref_identity_private_curve25519"; - public static boolean hasIdentityKey(Context context) { + public static boolean hasIdentityKey(Context context, int subscriptionId) { SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0); return - preferences.contains(IDENTITY_PUBLIC_KEY_DJB_PREF) && - preferences.contains(IDENTITY_PRIVATE_KEY_DJB_PREF); + preferences.contains(getIdentityPublicKeyDjbPref(subscriptionId)) && + preferences.contains(getIdentityPrivateKeyDjbPref(subscriptionId)); } - public static IdentityKey getIdentityKey(Context context) { - if (!hasIdentityKey(context)) return null; + public static IdentityKey getIdentityKey(Context context, int subscriptionId) { + if (!hasIdentityKey(context, subscriptionId)) return null; try { - byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_DJB_PREF)); + byte[] publicKeyBytes = Base64.decode(retrieve(context, getIdentityPublicKeyDjbPref(subscriptionId))); return new IdentityKey(publicKeyBytes, 0); } catch (IOException ioe) { - Log.w("IdentityKeyUtil", ioe); + Log.w(TAG, ioe); return null; } catch (InvalidKeyException e) { - Log.w("IdentityKeyUtil", e); + Log.w(TAG, e); return null; } } public static IdentityKeyPair getIdentityKeyPair(Context context, - MasterSecret masterSecret) + MasterSecret masterSecret, + int subscriptionId) { - if (!hasIdentityKey(context)) + if (!hasIdentityKey(context, subscriptionId)) return null; try { MasterCipher masterCipher = new MasterCipher(masterSecret); - IdentityKey publicKey = getIdentityKey(context); - ECPrivateKey privateKey = masterCipher.decryptKey(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_DJB_PREF))); + IdentityKey publicKey = getIdentityKey(context, subscriptionId); + ECPrivateKey privateKey = masterCipher.decryptKey(Base64.decode(retrieve(context, getIdentityPrivateKeyDjbPref(subscriptionId)))); return new IdentityKeyPair(publicKey, privateKey); } catch (IOException | InvalidKeyException e) { @@ -83,31 +87,38 @@ public class IdentityKeyUtil { } } - public static void generateIdentityKeys(Context context, MasterSecret masterSecret) { + public static void generateIdentityKeys(Context context, MasterSecret masterSecret, int subscriptionId) { + generateIdentityKeys(context, masterSecret, subscriptionId, true); + } + + public static void generateIdentityKeys(Context context, MasterSecret masterSecret, int subscriptionId, boolean displayNotification) { + Log.w(TAG, "Generating identity keys for subscription ID " + subscriptionId); ECKeyPair djbKeyPair = Curve.generateKeyPair(); MasterCipher masterCipher = new MasterCipher(masterSecret); IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey()); byte[] djbPrivateKey = masterCipher.encryptKey(djbKeyPair.getPrivateKey()); - save(context, IDENTITY_PUBLIC_KEY_DJB_PREF, Base64.encodeBytes(djbIdentityKey.serialize())); - save(context, IDENTITY_PRIVATE_KEY_DJB_PREF, Base64.encodeBytes(djbPrivateKey)); + save(context, getIdentityPublicKeyDjbPref(subscriptionId), Base64.encodeBytes(djbIdentityKey.serialize())); + save(context, getIdentityPrivateKeyDjbPref(subscriptionId), Base64.encodeBytes(djbPrivateKey)); + + if (displayNotification) DualSimUtil.displayNotification(context); } - public static boolean hasCurve25519IdentityKeys(Context context) { + public static boolean hasCurve25519IdentityKeys(Context context, int subscriptionId) { return - retrieve(context, IDENTITY_PUBLIC_KEY_DJB_PREF) != null && - retrieve(context, IDENTITY_PRIVATE_KEY_DJB_PREF) != null; + retrieve(context, getIdentityPublicKeyDjbPref(subscriptionId)) != null && + retrieve(context, getIdentityPrivateKeyDjbPref(subscriptionId)) != null; } - public static void generateCurve25519IdentityKeys(Context context, MasterSecret masterSecret) { + public static void generateCurve25519IdentityKeys(Context context, MasterSecret masterSecret, int subscriptionId) { MasterCipher masterCipher = new MasterCipher(masterSecret); ECKeyPair djbKeyPair = Curve.generateKeyPair(); IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey()); byte[] djbPrivateKey = masterCipher.encryptKey(djbKeyPair.getPrivateKey()); - save(context, IDENTITY_PUBLIC_KEY_DJB_PREF, Base64.encodeBytes(djbIdentityKey.serialize())); - save(context, IDENTITY_PRIVATE_KEY_DJB_PREF, Base64.encodeBytes(djbPrivateKey)); + save(context, getIdentityPublicKeyDjbPref(subscriptionId), Base64.encodeBytes(djbIdentityKey.serialize())); + save(context, getIdentityPrivateKeyDjbPref(subscriptionId), Base64.encodeBytes(djbPrivateKey)); } public static String retrieve(Context context, String key) { @@ -122,4 +133,28 @@ public class IdentityKeyUtil { preferencesEditor.putString(key, value); if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences"); } + + public static void remove(Context context, String key) { + SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0); + Editor preferencesEditor = preferences.edit(); + + preferencesEditor.remove(key); + if (!preferencesEditor.commit()) throw new AssertionError("failed to remove identity key/value to shared preferences"); + } + + public static String getIdentityPublicKeyDjbPref(int subscriptionId) { + if (Build.VERSION.SDK_INT >= 22 && subscriptionId != -1) { + return IDENTITY_PUBLIC_KEY_DJB_PREF + "_" + subscriptionId; + } else { + return IDENTITY_PUBLIC_KEY_DJB_PREF; + } + } + + public static String getIdentityPrivateKeyDjbPref(int subscriptionId) { + if (Build.VERSION.SDK_INT >= 22 && subscriptionId != -1) { + return IDENTITY_PRIVATE_KEY_DJB_PREF + "_" + subscriptionId; + } else { + return IDENTITY_PRIVATE_KEY_DJB_PREF; + } + } } diff --git a/src/org/smssecure/smssecure/crypto/KeyExchangeInitiator.java b/src/org/smssecure/smssecure/crypto/KeyExchangeInitiator.java index 9230ebcc3308abd595230d1bfbce60fdfa9d06de..3aa5d973860a3ed6f8e4411b555da742b6fc85ce 100644 --- a/src/org/smssecure/smssecure/crypto/KeyExchangeInitiator.java +++ b/src/org/smssecure/smssecure/crypto/KeyExchangeInitiator.java @@ -20,6 +20,10 @@ package org.smssecure.smssecure.crypto; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import android.os.Build; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.widget.Toast; import org.smssecure.smssecure.R; import org.smssecure.smssecure.crypto.SessionBuilder; @@ -31,7 +35,9 @@ import org.smssecure.smssecure.recipients.Recipient; import org.smssecure.smssecure.recipients.RecipientFactory; import org.smssecure.smssecure.recipients.Recipients; import org.smssecure.smssecure.sms.MessageSender; +import org.smssecure.smssecure.sms.OutgoingEndSessionMessage; import org.smssecure.smssecure.sms.OutgoingKeyExchangeMessage; +import org.smssecure.smssecure.sms.OutgoingTextMessage; import org.smssecure.smssecure.util.Base64; import org.smssecure.smssecure.util.ResUtil; import org.whispersystems.libsignal.SignalProtocolAddress; @@ -41,10 +47,28 @@ import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; import org.whispersystems.libsignal.state.SignedPreKeyStore; +import java.util.List; + public class KeyExchangeInitiator { + public static void abort(final Context context, final MasterSecret masterSecret, final Recipients recipients, final int subscriptionId) { + OutgoingEndSessionMessage endSessionMessage = new OutgoingEndSessionMessage(new OutgoingTextMessage(recipients, "TERMINATE", subscriptionId)); + MessageSender.send(context, masterSecret, endSessionMessage, -1, false); + } + + public static void initiate(final Context context, final MasterSecret masterSecret, final Recipients recipients, boolean promptOnExisting) { + if (Build.VERSION.SDK_INT >= 22) { + List listSubscriptionInfo = SubscriptionManager.from(context).getActiveSubscriptionInfoList(); + for (SubscriptionInfo subscriptionInfo : listSubscriptionInfo) { + initiate(context, masterSecret, recipients, promptOnExisting, subscriptionInfo.getSubscriptionId()); + } + } else { + initiate(context, masterSecret, recipients, promptOnExisting, -1); + } + } + public static void initiate(final Context context, final MasterSecret masterSecret, final Recipients recipients, boolean promptOnExisting, final int subscriptionId) { - if (promptOnExisting && hasInitiatedSession(context, masterSecret, recipients)) { + if (promptOnExisting && hasInitiatedSession(context, masterSecret, recipients, subscriptionId)) { AlertDialog.Builder dialog = new AlertDialog.Builder(context); dialog.setTitle(R.string.KeyExchangeInitiator_initiate_despite_existing_request_question); dialog.setMessage(R.string.KeyExchangeInitiator_youve_already_sent_a_session_initiation_request_to_this_recipient_are_you_sure); @@ -62,28 +86,33 @@ public class KeyExchangeInitiator { } } - private static void initiateKeyExchange(Context context, MasterSecret masterSecret, Recipients recipients, int subscriptionId) { + public static void initiateKeyExchange(Context context, MasterSecret masterSecret, Recipients recipients, int subscriptionId) { Recipient recipient = recipients.getPrimaryRecipient(); - SessionStore sessionStore = new SilenceSessionStore(context, masterSecret); - PreKeyStore preKeyStore = new SilencePreKeyStore(context, masterSecret); - SignedPreKeyStore signedPreKeyStore = new SilencePreKeyStore(context, masterSecret); - IdentityKeyStore identityKeyStore = new SilenceIdentityKeyStore(context, masterSecret); + SessionStore sessionStore = new SilenceSessionStore(context, masterSecret, subscriptionId); + PreKeyStore preKeyStore = new SilencePreKeyStore(context, masterSecret, subscriptionId); + SignedPreKeyStore signedPreKeyStore = new SilencePreKeyStore(context, masterSecret, subscriptionId); + IdentityKeyStore identityKeyStore = new SilenceIdentityKeyStore(context, masterSecret, subscriptionId); SessionBuilder sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore, identityKeyStore, new SignalProtocolAddress(recipient.getNumber(), 1)); - KeyExchangeMessage keyExchangeMessage = sessionBuilder.process(); - String serializedMessage = Base64.encodeBytesWithoutPadding(keyExchangeMessage.serialize()); - OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipients, serializedMessage, subscriptionId); + if (identityKeyStore.getIdentityKeyPair() != null) { + KeyExchangeMessage keyExchangeMessage = sessionBuilder.process(); + String serializedMessage = Base64.encodeBytesWithoutPadding(keyExchangeMessage.serialize()); + OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipients, serializedMessage, subscriptionId); - MessageSender.send(context, masterSecret, textMessage, -1, false); + MessageSender.send(context, masterSecret, textMessage, -1, false); + } else { + Toast.makeText(context, R.string.VerifyIdentityActivity_you_do_not_have_an_identity_key, + Toast.LENGTH_LONG).show(); + } } private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret, - Recipients recipients) + Recipients recipients, int subscriptionId) { Recipient recipient = recipients.getPrimaryRecipient(); - SessionStore sessionStore = new SilenceSessionStore(context, masterSecret); + SessionStore sessionStore = new SilenceSessionStore(context, masterSecret, subscriptionId); SessionRecord sessionRecord = sessionStore.loadSession(new SignalProtocolAddress(recipient.getNumber(), 1)); return sessionRecord.getSessionState().hasPendingKeyExchange(); diff --git a/src/org/smssecure/smssecure/crypto/PreKeyUtil.java b/src/org/smssecure/smssecure/crypto/PreKeyUtil.java deleted file mode 100644 index 66baa3f1a7b033e167da3fdc337cf255f90525d8..0000000000000000000000000000000000000000 --- a/src/org/smssecure/smssecure/crypto/PreKeyUtil.java +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Copyright (C) 2013 Open 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.smssecure.smssecure.crypto; - -import android.content.Context; -import android.util.Log; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import org.smssecure.smssecure.crypto.storage.SilencePreKeyStore; -import org.smssecure.smssecure.util.JsonUtils; -import org.smssecure.smssecure.util.Util; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.ecc.Curve; -import org.whispersystems.libsignal.ecc.ECKeyPair; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.PreKeyStore; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyStore; -import org.whispersystems.libsignal.util.Medium; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.LinkedList; -import java.util.List; - -public class PreKeyUtil { - - public static final int BATCH_SIZE = 100; - - public static List generatePreKeys(Context context, MasterSecret masterSecret) { - PreKeyStore preKeyStore = new SilencePreKeyStore(context, masterSecret); - List records = new LinkedList<>(); - int preKeyIdOffset = getNextPreKeyId(context); - - for (int i=0;i activeSubscriptions) { + if (Build.VERSION.SDK_INT >= 22) { + for (SubscriptionInfoCompat subscriptionInfo : activeSubscriptions) { + if (!hasSession(context, masterSecret, number, subscriptionInfo.getSubscriptionId())) return false; + } + return true; + } else { + return hasSession(context, masterSecret, number, -1); + } + } + + public static boolean hasAtLeastOneSession(Context context, MasterSecret masterSecret, @NonNull String number, List activeSubscriptions) { + if (Build.VERSION.SDK_INT >= 22) { + for (SubscriptionInfoCompat subscriptionInfo : activeSubscriptions) { + if (hasSession(context, masterSecret, number, subscriptionInfo.getSubscriptionId())) return true; + } + return false; + } else { + return hasSession(context, masterSecret, number, -1); + } + } + + @TargetApi(22) + public static List getSubscriptionIdWithoutSession(Context context, MasterSecret masterSecret, @NonNull String number, List activeSubscriptions) { + LinkedList list = new LinkedList(); + + for (SubscriptionInfoCompat subscriptionInfo : activeSubscriptions) { + int subscriptionId = subscriptionInfo.getSubscriptionId(); + if (!hasSession(context, masterSecret, number, subscriptionId)) list.add(subscriptionId); + } + return list; + } } diff --git a/src/org/smssecure/smssecure/crypto/storage/SilenceIdentityKeyStore.java b/src/org/smssecure/smssecure/crypto/storage/SilenceIdentityKeyStore.java index fbbe947c8d5205fe278c7769e2355069f51fb830..f2eee3b8239ec8c5413efe7a59f626e8d1b2a598 100644 --- a/src/org/smssecure/smssecure/crypto/storage/SilenceIdentityKeyStore.java +++ b/src/org/smssecure/smssecure/crypto/storage/SilenceIdentityKeyStore.java @@ -16,15 +16,17 @@ public class SilenceIdentityKeyStore implements IdentityKeyStore { private final Context context; private final MasterSecret masterSecret; + private final int subscriptionId; - public SilenceIdentityKeyStore(Context context, MasterSecret masterSecret) { - this.context = context; - this.masterSecret = masterSecret; + public SilenceIdentityKeyStore(Context context, MasterSecret masterSecret, int subscriptionId) { + this.context = context; + this.masterSecret = masterSecret; + this.subscriptionId = subscriptionId; } @Override public IdentityKeyPair getIdentityKeyPair() { - return IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); + return IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, subscriptionId); } @Override diff --git a/src/org/smssecure/smssecure/crypto/storage/SilencePreKeyStore.java b/src/org/smssecure/smssecure/crypto/storage/SilencePreKeyStore.java index 16a2807e1fe88c7e6ffa140f3b3658c47cf65987..bb79457b06c5cfc56a60490efd9453472c316d3e 100644 --- a/src/org/smssecure/smssecure/crypto/storage/SilencePreKeyStore.java +++ b/src/org/smssecure/smssecure/crypto/storage/SilencePreKeyStore.java @@ -34,10 +34,12 @@ public class SilencePreKeyStore implements PreKeyStore, SignedPreKeyStore { private final Context context; private final MasterSecret masterSecret; + private final int subscriptionId; - public SilencePreKeyStore(Context context, MasterSecret masterSecret) { - this.context = context; - this.masterSecret = masterSecret; + public SilencePreKeyStore(Context context, MasterSecret masterSecret, int subscriptionId) { + this.context = context; + this.masterSecret = masterSecret; + this.subscriptionId = subscriptionId; } @Override @@ -156,11 +158,13 @@ public class SilencePreKeyStore implements PreKeyStore, SignedPreKeyStore { } private File getPreKeyFile(int preKeyId) { - return new File(getPreKeyDirectory(), String.valueOf(preKeyId)); + String subscriptionFile = subscriptionId != -1 ? subscriptionId + "" : ""; + return new File(getPreKeyDirectory(), String.valueOf(preKeyId) + subscriptionFile); } private File getSignedPreKeyFile(int signedPreKeyId) { - return new File(getSignedPreKeyDirectory(), String.valueOf(signedPreKeyId)); + String subscriptionFile = subscriptionId != -1 ? subscriptionId + "" : ""; + return new File(getSignedPreKeyDirectory(), String.valueOf(signedPreKeyId) + subscriptionFile); } private File getPreKeyDirectory() { diff --git a/src/org/smssecure/smssecure/crypto/storage/SilenceSessionStore.java b/src/org/smssecure/smssecure/crypto/storage/SilenceSessionStore.java index 742b423502653513d0b783778805c9ec3215844b..2336cdd19f06333f96da2f3883aab84bf7bd923f 100644 --- a/src/org/smssecure/smssecure/crypto/storage/SilenceSessionStore.java +++ b/src/org/smssecure/smssecure/crypto/storage/SilenceSessionStore.java @@ -1,6 +1,7 @@ package org.smssecure.smssecure.crypto.storage; import android.content.Context; +import android.os.Build; import android.util.Log; import org.smssecure.smssecure.crypto.MasterCipher; @@ -37,10 +38,15 @@ public class SilenceSessionStore implements SessionStore { private final Context context; private final MasterSecret masterSecret; + private final int subscriptionId; - public SilenceSessionStore(Context context, MasterSecret masterSecret) { - this.context = context.getApplicationContext(); - this.masterSecret = masterSecret; + public SilenceSessionStore(Context context, MasterSecret masterSecret, int subscriptionId) { + Log.w(TAG, "SilenceSessionStore for subscription ID " + subscriptionId); + if (subscriptionId == -1) Log.w(TAG, "Subscription ID should not be -1!"); + + this.context = context.getApplicationContext(); + this.masterSecret = masterSecret; + this.subscriptionId = subscriptionId; } @Override @@ -143,10 +149,15 @@ public class SilenceSessionStore implements SessionStore { } private File getSessionFile(SignalProtocolAddress address) { - return new File(getSessionDirectory(), getSessionName(address)); + String sessionName = getSessionName(address); + return new File(getSessionDirectory(), sessionName); } private File getSessionDirectory() { + return getSessionDirectory(context); + } + + public static File getSessionDirectory(Context context) { File directory = new File(context.getFilesDir(), SESSIONS_DIRECTORY_V2); if (!directory.exists()) { @@ -159,12 +170,10 @@ public class SilenceSessionStore implements SessionStore { } private String getSessionName(SignalProtocolAddress axolotlAddress) { - Recipient recipient = RecipientFactory.getRecipientsFromString(context, axolotlAddress.getName(), true) - .getPrimaryRecipient(); + Recipient recipient = RecipientFactory.getRecipientsFromString(context, axolotlAddress.getName(), true).getPrimaryRecipient(); long recipientId = recipient.getRecipientId(); - int deviceId = axolotlAddress.getDeviceId(); - return recipientId + (deviceId == 1 ? "" : "." + deviceId); + return recipientId + ((Build.VERSION.SDK_INT < 22 || subscriptionId == -1) ? "" : "." + subscriptionId); } private byte[] readBlob(FileInputStream in) throws IOException { diff --git a/src/org/smssecure/smssecure/crypto/storage/SilenceSignalProtocolStore.java b/src/org/smssecure/smssecure/crypto/storage/SilenceSignalProtocolStore.java index 4c3e62fcd88ee73fc2ec4cd28b768e212023f23e..9c8917503aa618f541a20fc32484e09490f4d368 100644 --- a/src/org/smssecure/smssecure/crypto/storage/SilenceSignalProtocolStore.java +++ b/src/org/smssecure/smssecure/crypto/storage/SilenceSignalProtocolStore.java @@ -24,12 +24,14 @@ public class SilenceSignalProtocolStore implements SignalProtocolStore { private final SignedPreKeyStore signedPreKeyStore; private final IdentityKeyStore identityKeyStore; private final SessionStore sessionStore; - - public SilenceSignalProtocolStore(Context context, MasterSecret masterSecret) { - this.preKeyStore = new SilencePreKeyStore(context, masterSecret); - this.signedPreKeyStore = new SilencePreKeyStore(context, masterSecret); - this.identityKeyStore = new SilenceIdentityKeyStore(context, masterSecret); - this.sessionStore = new SilenceSessionStore(context, masterSecret); + private final int subscriptionId; + + public SilenceSignalProtocolStore(Context context, MasterSecret masterSecret, int subscriptionId) { + this.preKeyStore = new SilencePreKeyStore(context, masterSecret, subscriptionId); + this.signedPreKeyStore = new SilencePreKeyStore(context, masterSecret, subscriptionId); + this.identityKeyStore = new SilenceIdentityKeyStore(context, masterSecret, subscriptionId); + this.sessionStore = new SilenceSessionStore(context, masterSecret, subscriptionId); + this.subscriptionId = subscriptionId; } @Override diff --git a/src/org/smssecure/smssecure/database/DatabaseFactory.java b/src/org/smssecure/smssecure/database/DatabaseFactory.java index 91c225d7ad03449cfbb8004c4e30a3e672c6a901..d4df5f1b76828effc36a7e41be865d32e9866b11 100644 --- a/src/org/smssecure/smssecure/database/DatabaseFactory.java +++ b/src/org/smssecure/smssecure/database/DatabaseFactory.java @@ -207,281 +207,7 @@ public class DatabaseFactory { SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.beginTransaction(); - if (fromVersion < DatabaseUpgradeActivity.NO_MORE_KEY_EXCHANGE_PREFIX_VERSION) { - String KEY_EXCHANGE = "?SilenceKeyExchange"; - String PROCESSED_KEY_EXCHANGE = "?SilenceKeyExchangd"; - String STALE_KEY_EXCHANGE = "?SilenceKeyExchangs"; - int ROW_LIMIT = 500; - - MasterCipher masterCipher = new MasterCipher(masterSecret); - int smsCount = 0; - int threadCount = 0; - int skip = 0; - - Cursor cursor = db.query("sms", new String[] {"COUNT(*)"}, "type & " + 0x80000000 + " != 0", - null, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - smsCount = cursor.getInt(0); - cursor.close(); - } - - cursor = db.query("thread", new String[] {"COUNT(*)"}, "snippet_type & " + 0x80000000 + " != 0", - null, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - threadCount = cursor.getInt(0); - cursor.close(); - } - - Cursor smsCursor = null; - - Log.w(TAG, "Upgrade count: " + (smsCount + threadCount)); - - do { - Log.w(TAG, "Looping SMS cursor..."); - if (smsCursor != null) - smsCursor.close(); - - smsCursor = db.query("sms", new String[] {"_id", "type", "body"}, - "type & " + 0x80000000 + " != 0", - null, null, null, "_id", skip + "," + ROW_LIMIT); - - while (smsCursor != null && smsCursor.moveToNext()) { - listener.setProgress(smsCursor.getPosition() + skip, smsCount + threadCount); - - try { - String eBody = smsCursor.getString(smsCursor.getColumnIndexOrThrow("body")); - String body = eBody != null ? masterCipher.decryptBody(eBody) : ""; - long type = smsCursor.getLong(smsCursor.getColumnIndexOrThrow("type")); - long id = smsCursor.getLong(smsCursor.getColumnIndexOrThrow("_id")); - - if (body.startsWith(KEY_EXCHANGE)) { - body = body.substring(KEY_EXCHANGE.length()); - body = masterCipher.encryptBody(body); - type |= 0x8000; - - db.execSQL("UPDATE sms SET body = ?, type = ? WHERE _id = ?", - new String[] {body, type+"", id+""}); - } else if (body.startsWith(PROCESSED_KEY_EXCHANGE)) { - body = body.substring(PROCESSED_KEY_EXCHANGE.length()); - body = masterCipher.encryptBody(body); - type |= (0x8000 | 0x2000); - - db.execSQL("UPDATE sms SET body = ?, type = ? WHERE _id = ?", - new String[] {body, type+"", id+""}); - } else if (body.startsWith(STALE_KEY_EXCHANGE)) { - body = body.substring(STALE_KEY_EXCHANGE.length()); - body = masterCipher.encryptBody(body); - type |= (0x8000 | 0x4000); - - db.execSQL("UPDATE sms SET body = ?, type = ? WHERE _id = ?", - new String[] {body, type+"", id+""}); - } - } catch (InvalidMessageException e) { - Log.w(TAG, e); - } - } - - skip += ROW_LIMIT; - } while (smsCursor != null && smsCursor.getCount() > 0); - - - - Cursor threadCursor = null; - skip = 0; - - do { - Log.w(TAG, "Looping thread cursor..."); - - if (threadCursor != null) - threadCursor.close(); - - threadCursor = db.query("thread", new String[] {"_id", "snippet_type", "snippet"}, - "snippet_type & " + 0x80000000 + " != 0", - null, null, null, "_id", skip + "," + ROW_LIMIT); - - while (threadCursor != null && threadCursor.moveToNext()) { - listener.setProgress(smsCount + threadCursor.getPosition(), smsCount + threadCount); - - try { - String snippet = threadCursor.getString(threadCursor.getColumnIndexOrThrow("snippet")); - long snippetType = threadCursor.getLong(threadCursor.getColumnIndexOrThrow("snippet_type")); - long id = threadCursor.getLong(threadCursor.getColumnIndexOrThrow("_id")); - - if (!TextUtils.isEmpty(snippet)) { - snippet = masterCipher.decryptBody(snippet); - } - - if (snippet.startsWith(KEY_EXCHANGE)) { - snippet = snippet.substring(KEY_EXCHANGE.length()); - snippet = masterCipher.encryptBody(snippet); - snippetType |= 0x8000; - - db.execSQL("UPDATE thread SET snippet = ?, snippet_type = ? WHERE _id = ?", - new String[] {snippet, snippetType+"", id+""}); - } else if (snippet.startsWith(PROCESSED_KEY_EXCHANGE)) { - snippet = snippet.substring(PROCESSED_KEY_EXCHANGE.length()); - snippet = masterCipher.encryptBody(snippet); - snippetType |= (0x8000 | 0x2000); - - db.execSQL("UPDATE thread SET snippet = ?, snippet_type = ? WHERE _id = ?", - new String[] {snippet, snippetType+"", id+""}); - } else if (snippet.startsWith(STALE_KEY_EXCHANGE)) { - snippet = snippet.substring(STALE_KEY_EXCHANGE.length()); - snippet = masterCipher.encryptBody(snippet); - snippetType |= (0x8000 | 0x4000); - - db.execSQL("UPDATE thread SET snippet = ?, snippet_type = ? WHERE _id = ?", - new String[] {snippet, snippetType+"", id+""}); - } - } catch (InvalidMessageException e) { - Log.w(TAG, e); - } - } - - skip += ROW_LIMIT; - } while (threadCursor != null && threadCursor.getCount() > 0); - - if (smsCursor != null) - smsCursor.close(); - - if (threadCursor != null) - threadCursor.close(); - } - - if (fromVersion < DatabaseUpgradeActivity.MMS_BODY_VERSION) { - Log.w(TAG, "Update MMS bodies..."); - MasterCipher masterCipher = new MasterCipher(masterSecret); - Cursor mmsCursor = db.query("mms", new String[] {"_id"}, - "msg_box & " + 0x80000000L + " != 0", - null, null, null, null); - - Log.w(TAG, "Got MMS rows: " + (mmsCursor == null ? "null" : mmsCursor.getCount())); - - while (mmsCursor != null && mmsCursor.moveToNext()) { - listener.setProgress(mmsCursor.getPosition(), mmsCursor.getCount()); - - long mmsId = mmsCursor.getLong(mmsCursor.getColumnIndexOrThrow("_id")); - String body = null; - int partCount = 0; - Cursor partCursor = db.query("part", new String[] {"_id", "ct", "_data", "encrypted"}, - "mid = ?", new String[] {mmsId+""}, null, null, null); - - while (partCursor != null && partCursor.moveToNext()) { - String contentType = partCursor.getString(partCursor.getColumnIndexOrThrow("ct")); - - if (ContentType.isTextType(contentType)) { - try { - long partId = partCursor.getLong(partCursor.getColumnIndexOrThrow("_id")); - String dataLocation = partCursor.getString(partCursor.getColumnIndexOrThrow("_data")); - boolean encrypted = partCursor.getInt(partCursor.getColumnIndexOrThrow("encrypted")) == 1; - File dataFile = new File(dataLocation); - - InputStream is; - - if (encrypted) is = new DecryptingPartInputStream(dataFile, masterSecret); - else is = new FileInputStream(dataFile); - - body = (body == null) ? Util.readFullyAsString(is) : body + " " + Util.readFullyAsString(is); - - //noinspection ResultOfMethodCallIgnored - dataFile.delete(); - db.delete("part", "_id = ?", new String[] {partId+""}); - } catch (IOException e) { - Log.w(TAG, e); - } - } else if (ContentType.isAudioType(contentType) || - ContentType.isImageType(contentType) || - ContentType.isVideoType(contentType)) - { - partCount++; - } - } - - if (!TextUtils.isEmpty(body)) { - body = masterCipher.encryptBody(body); - db.execSQL("UPDATE mms SET body = ?, part_count = ? WHERE _id = ?", - new String[] {body, partCount+"", mmsId+""}); - } else { - db.execSQL("UPDATE mms SET part_count = ? WHERE _id = ?", - new String[] {partCount+"", mmsId+""}); - } - - Log.w(TAG, "Updated body: " + body + " and part_count: " + partCount); - } - } - - if (fromVersion < DatabaseUpgradeActivity.TOFU_IDENTITIES_VERSION) { - File sessionDirectory = new File(context.getFilesDir() + File.separator + "sessions"); - - if (sessionDirectory.exists() && sessionDirectory.isDirectory()) { - File[] sessions = sessionDirectory.listFiles(); - - if (sessions != null) { - for (File session : sessions) { - String name = session.getName(); - - if (name.matches("[0-9]+")) { - long recipientId = Long.parseLong(name); - IdentityKey identityKey = null; - // NOTE (4/21/14) -- At this moment in time, we're forgetting the ability to parse - // V1 session records. Despite our usual attempts to avoid using shared code in the - // upgrade path, this is too complex to put here directly. Thus, unfortunately - // this operation is now lost to the ages. From the git log, it seems to have been - // almost exactly a year since this went in, so hopefully the bulk of people have - // already upgraded. -// IdentityKey identityKey = Session.getRemoteIdentityKey(context, masterSecret, recipientId); - - if (identityKey != null) { - MasterCipher masterCipher = new MasterCipher(masterSecret); - String identityKeyString = Base64.encodeBytes(identityKey.serialize()); - String macString = Base64.encodeBytes(masterCipher.getMacFor(recipientId + - identityKeyString)); - - db.execSQL("REPLACE INTO identities (recipient, key, mac) VALUES (?, ?, ?)", - new String[] {recipientId+"", identityKeyString, macString}); - } - } - } - } - } - } - - if (fromVersion < DatabaseUpgradeActivity.ASYMMETRIC_MASTER_SECRET_FIX_VERSION) { - if (!MasterSecretUtil.hasAsymmericMasterSecret(context)) { - MasterSecretUtil.generateAsymmetricMasterSecret(context, masterSecret); - - MasterCipher masterCipher = new MasterCipher(masterSecret); - Cursor cursor = null; - - try { - cursor = db.query(SmsDatabase.TABLE_NAME, - new String[] {SmsDatabase.ID, SmsDatabase.BODY, SmsDatabase.TYPE}, - SmsDatabase.TYPE + " & ? == 0", - new String[] {String.valueOf(SmsDatabase.Types.ENCRYPTION_MASK)}, - null, null, null); - - while (cursor.moveToNext()) { - long id = cursor.getLong(0); - String body = cursor.getString(1); - long type = cursor.getLong(2); - - String encryptedBody = masterCipher.encryptBody(body); - - ContentValues update = new ContentValues(); - update.put(SmsDatabase.BODY, encryptedBody); - update.put(SmsDatabase.TYPE, type | SmsDatabase.Types.ENCRYPTION_SYMMETRIC_BIT); - - db.update(SmsDatabase.TABLE_NAME, update, SmsDatabase.ID + " = ?", - new String[] {String.valueOf(id)}); - } - } finally { - if (cursor != null) - cursor.close(); - } - } - } + // Do stuff here db.setTransactionSuccessful(); db.endTransaction(); diff --git a/src/org/smssecure/smssecure/database/MmsSmsDatabase.java b/src/org/smssecure/smssecure/database/MmsSmsDatabase.java index 6f79bc22fe5789153b194bab6ef616797a6299c4..f30630248bf58e670d2b12237336ad7319a0c0f8 100644 --- a/src/org/smssecure/smssecure/database/MmsSmsDatabase.java +++ b/src/org/smssecure/smssecure/database/MmsSmsDatabase.java @@ -23,7 +23,6 @@ import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; import org.smssecure.smssecure.crypto.MasterSecret; import org.smssecure.smssecure.database.model.MessageRecord; @@ -34,8 +33,6 @@ import java.util.Set; public class MmsSmsDatabase extends Database { - private static final String TAG = MmsSmsDatabase.class.getSimpleName(); - public static final String TRANSPORT = "transport_type"; public static final String MMS_TRANSPORT = "mms"; public static final String SMS_TRANSPORT = "sms"; @@ -246,7 +243,6 @@ public class MmsSmsDatabase extends Database { @SuppressWarnings("deprecation") String query = outerQueryBuilder.buildQuery(projection, null, null, null, null, null, null); - Log.w("MmsSmsDatabase", "Executing query: " + query); SQLiteDatabase db = databaseHelper.getReadableDatabase(); return db.rawQuery(query, null); } diff --git a/src/org/smssecure/smssecure/database/SmsDatabase.java b/src/org/smssecure/smssecure/database/SmsDatabase.java index ff077a644abee4d77aecf247d8f21d5d06ec2cd0..c8e5825abd649bf220f0312a4f629cc0b6f06e97 100644 --- a/src/org/smssecure/smssecure/database/SmsDatabase.java +++ b/src/org/smssecure/smssecure/database/SmsDatabase.java @@ -396,7 +396,7 @@ public class SmsDatabase extends MessagingDatabase { if (groupRecipients == null) threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); else threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients); - ContentValues values = new ContentValues(6); + ContentValues values = new ContentValues(); values.put(ADDRESS, message.getSender()); values.put(ADDRESS_DEVICE_ID, message.getSenderDeviceId()); values.put(DATE_RECEIVED, System.currentTimeMillis()); diff --git a/src/org/smssecure/smssecure/jobs/GenerateKeysJob.java b/src/org/smssecure/smssecure/jobs/GenerateKeysJob.java new file mode 100644 index 0000000000000000000000000000000000000000..e3939a05a67a67e43f86d35d74fe518097cce446 --- /dev/null +++ b/src/org/smssecure/smssecure/jobs/GenerateKeysJob.java @@ -0,0 +1,47 @@ +package org.smssecure.smssecure.jobs; + +import android.content.Context; +import android.util.Log; + +import org.smssecure.smssecure.crypto.MasterSecret; +import org.smssecure.smssecure.jobs.requirements.MasterSecretRequirement; +import org.smssecure.smssecure.util.dualsim.DualSimUtil; +import org.smssecure.smssecure.util.dualsim.SubscriptionInfoCompat; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; +import org.whispersystems.jobqueue.JobParameters; + + +import java.util.List; + +public class GenerateKeysJob extends MasterSecretJob { + private static final String TAG = GenerateKeysJob.class.getSimpleName(); + + private List activeSubscriptions; + + public GenerateKeysJob(Context context) { + super(context, JobParameters.newBuilder() + .withPersistence() + .withRequirement(new MasterSecretRequirement(context)) + .create()); + + this.activeSubscriptions = activeSubscriptions; + } + + @Override + public void onAdded() {} + + @Override + public void onRun(MasterSecret masterSecret) { + Log.w(TAG, "onRun()"); + List activeSubscriptions = SubscriptionManagerCompat.from(context).updateActiveSubscriptionInfoList(); + DualSimUtil.generateKeysIfDoNotExist(context, masterSecret, activeSubscriptions); + } + + @Override + public boolean onShouldRetryThrowable(Exception exception) { + return false; + } + + @Override + public void onCanceled() {} +} diff --git a/src/org/smssecure/smssecure/jobs/MmsDownloadJob.java b/src/org/smssecure/smssecure/jobs/MmsDownloadJob.java index cfd295f789d9d5594cfa1d185eb6a6381ade3cb9..6580f87f75e43fde193c718e985cd18518c96089 100644 --- a/src/org/smssecure/smssecure/jobs/MmsDownloadJob.java +++ b/src/org/smssecure/smssecure/jobs/MmsDownloadJob.java @@ -119,7 +119,7 @@ public class MmsDownloadJob extends MasterSecretJob { } if (retrieveConf.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieveConf.getSubject().getString())) { - MmsCipher mmsCipher = new MmsCipher(new SilenceSignalProtocolStore(context, masterSecret)); + MmsCipher mmsCipher = new MmsCipher(new SilenceSignalProtocolStore(context, masterSecret, notification.get().second)); RetrieveConf plaintextPdu = (RetrieveConf) mmsCipher.decrypt(context, retrieveConf); storeRetrievedMms(masterSecret, contentLocation, messageId, threadId, plaintextPdu, true, notification.get().second); diff --git a/src/org/smssecure/smssecure/jobs/MmsSendJob.java b/src/org/smssecure/smssecure/jobs/MmsSendJob.java index 2ae857ac54c332a9c26f73e65749249404c97629..9dd887993af49999e2f48f15a6981fd93c6557d3 100644 --- a/src/org/smssecure/smssecure/jobs/MmsSendJob.java +++ b/src/org/smssecure/smssecure/jobs/MmsSendJob.java @@ -23,6 +23,7 @@ import org.smssecure.smssecure.recipients.Recipient; import org.smssecure.smssecure.recipients.Recipients; import org.smssecure.smssecure.recipients.RecipientFormattingException; import org.smssecure.smssecure.transport.UndeliverableMessageException; +import org.smssecure.smssecure.util.dualsim.DualSimUtil; import org.smssecure.smssecure.util.Hex; import org.smssecure.smssecure.util.NumberUtil; import org.smssecure.smssecure.util.SmilUtil; @@ -84,14 +85,14 @@ public class MmsSendJob extends SendJob { if (message.isSecure()) { Log.w(TAG, "Encrypting MMS..."); - pdu = getEncryptedMessage(masterSecret, pdu); + pdu = getEncryptedMessage(masterSecret, pdu, message.getSubscriptionId()); upgradedSecure = true; } validateDestinations(message, pdu); final byte[] pduBytes = getPduBytes(masterSecret, pdu); - final SendConf sendConf = new CompatMmsConnection(context).send(pduBytes, message.getSubscriptionId()); + final SendConf sendConf = new CompatMmsConnection(context).send(pduBytes, DualSimUtil.getSubscriptionIdFromAppSubscriptionId(context, message.getSubscriptionId())); final MmsSendResult result = getSendResult(sendConf, pdu, upgradedSecure); database.markAsSent(messageId, result.isUpgradedSecure()); @@ -148,11 +149,11 @@ public class MmsSendJob extends SendJob { } } - private SendReq getEncryptedMessage(MasterSecret masterSecret, SendReq pdu) + private SendReq getEncryptedMessage(MasterSecret masterSecret, SendReq pdu, int subscriptionId) throws UndeliverableMessageException { try { - MmsCipher cipher = new MmsCipher(new SilenceSignalProtocolStore(context, masterSecret)); + MmsCipher cipher = new MmsCipher(new SilenceSignalProtocolStore(context, masterSecret, subscriptionId)); return cipher.encrypt(context, pdu); } catch (NoSessionException e) { throw new UndeliverableMessageException(e); diff --git a/src/org/smssecure/smssecure/jobs/SmsDecryptJob.java b/src/org/smssecure/smssecure/jobs/SmsDecryptJob.java index c927ded0e1085f40bc1dc2c19d4b13b8c4758ce3..e253326742d16754581c5250896db94c1563aff3 100644 --- a/src/org/smssecure/smssecure/jobs/SmsDecryptJob.java +++ b/src/org/smssecure/smssecure/jobs/SmsDecryptJob.java @@ -1,6 +1,7 @@ package org.smssecure.smssecure.jobs; import android.content.Context; +import android.os.Build; import android.util.Log; import org.smssecure.smssecure.crypto.AsymmetricMasterCipher; @@ -28,6 +29,8 @@ import org.smssecure.smssecure.sms.IncomingTextMessage; import org.smssecure.smssecure.sms.IncomingXmppExchangeMessage; import org.smssecure.smssecure.sms.MessageSender; import org.smssecure.smssecure.sms.OutgoingKeyExchangeMessage; +import org.smssecure.smssecure.util.dualsim.SubscriptionInfoCompat; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; import org.smssecure.smssecure.util.SilencePreferences; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.libsignal.DuplicateMessageException; @@ -40,6 +43,7 @@ import org.whispersystems.libsignal.UntrustedIdentityException; import org.whispersystems.libsignal.util.guava.Optional; import java.io.IOException; +import java.util.List; public class SmsDecryptJob extends MasterSecretJob { @@ -127,7 +131,7 @@ public class SmsDecryptJob extends MasterSecretJob { InvalidMessageException, LegacyMessageException { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); - SmsCipher cipher = new SmsCipher(new SilenceSignalProtocolStore(context, masterSecret)); + SmsCipher cipher = new SmsCipher(new SilenceSignalProtocolStore(context, masterSecret, message.getSubscriptionId())); IncomingTextMessage plaintext = cipher.decrypt(context, message); database.updateMessageBody(masterSecret, messageId, plaintext.getMessageBody()); @@ -143,7 +147,7 @@ public class SmsDecryptJob extends MasterSecretJob { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); try { - SmsCipher smsCipher = new SmsCipher(new SilenceSignalProtocolStore(context, masterSecret)); + SmsCipher smsCipher = new SmsCipher(new SilenceSignalProtocolStore(context, masterSecret, message.getSubscriptionId())); IncomingEncryptedMessage plaintext = smsCipher.decrypt(context, message); database.updateBundleMessageBody(masterSecret, messageId, plaintext.getMessageBody()); @@ -158,12 +162,12 @@ public class SmsDecryptJob extends MasterSecretJob { } private void handleKeyExchangeMessage(MasterSecret masterSecret, long messageId, long threadId, - IncomingKeyExchangeMessage message) + IncomingKeyExchangeMessage message) { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); try { - SmsCipher cipher = new SmsCipher(new SilenceSignalProtocolStore(context, masterSecret)); + SmsCipher cipher = new SmsCipher(new SilenceSignalProtocolStore(context, masterSecret, message.getSubscriptionId())); OutgoingKeyExchangeMessage response = cipher.process(context, message); if (shouldSend()) { @@ -209,7 +213,7 @@ public class SmsDecryptJob extends MasterSecretJob { database.markAsXmppExchange(messageId); } - private String getAsymmetricDecryptedBody(MasterSecret masterSecret, String body) + private String getAsymmetricDecryptedBody(MasterSecret masterSecret, String body, int subscriptionId) throws InvalidMessageException { try { @@ -228,13 +232,14 @@ public class SmsDecryptJob extends MasterSecretJob { String plaintextBody = record.getBody().getBody(); if (record.isAsymmetricEncryption()) { - plaintextBody = getAsymmetricDecryptedBody(masterSecret, record.getBody().getBody()); + plaintextBody = getAsymmetricDecryptedBody(masterSecret, record.getBody().getBody(), record.getSubscriptionId()); } IncomingTextMessage message = new IncomingTextMessage(record.getRecipients().getPrimaryRecipient().getNumber(), record.getRecipientDeviceId(), record.getDateSent(), - plaintextBody); + plaintextBody, + record.getSubscriptionId()); if (record.isEndSession()) { return new IncomingEndSessionMessage(message); diff --git a/src/org/smssecure/smssecure/jobs/SmsReceiveJob.java b/src/org/smssecure/smssecure/jobs/SmsReceiveJob.java index 2ae066c71759bf4c746de21326f95108428f3c1f..82c0a3480a50257ec047c996caaeaa14b4fbf642 100644 --- a/src/org/smssecure/smssecure/jobs/SmsReceiveJob.java +++ b/src/org/smssecure/smssecure/jobs/SmsReceiveJob.java @@ -17,6 +17,7 @@ import org.smssecure.smssecure.recipients.Recipients; import org.smssecure.smssecure.service.KeyCachingService; import org.smssecure.smssecure.sms.IncomingTextMessage; import org.smssecure.smssecure.sms.MultipartSmsMessageHandler; +import org.smssecure.smssecure.util.dualsim.DualSimUtil; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.libsignal.util.guava.Optional; @@ -40,8 +41,11 @@ public class SmsReceiveJob extends ContextJob { .withWakeLock(true) .create()); + Log.w(TAG, "subscriptionId: " + subscriptionId); + Log.w(TAG, "Found app subscription ID: " + DualSimUtil.getSubscriptionIdFromDeviceSubscriptionId(context, subscriptionId)); + this.pdus = pdus; - this.subscriptionId = subscriptionId; + this.subscriptionId = DualSimUtil.getSubscriptionIdFromDeviceSubscriptionId(context, subscriptionId); } @Override @@ -63,6 +67,12 @@ public class SmsReceiveJob extends ContextJob { { MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); } + + if (incomingTextMessage.getSender() != null) { + Recipients recipients = RecipientFactory.getRecipientsFromString(context, incomingTextMessage.getSender(), false); + DatabaseFactory.getRecipientPreferenceDatabase(context) + .setDefaultSubscriptionId(recipients, incomingTextMessage.getSubscriptionId()); + } } else if (message.isPresent()) { Log.w(TAG, "*** Received blocked SMS, ignoring..."); } diff --git a/src/org/smssecure/smssecure/jobs/SmsSendJob.java b/src/org/smssecure/smssecure/jobs/SmsSendJob.java index 2cae055eec06eddf0a3dbc1dcef8664c006ba587..a37efcfd45fb170dae4b6b12cc2bd857c724ba3a 100644 --- a/src/org/smssecure/smssecure/jobs/SmsSendJob.java +++ b/src/org/smssecure/smssecure/jobs/SmsSendJob.java @@ -26,6 +26,7 @@ import org.smssecure.smssecure.service.SmsDeliveryListener; import org.smssecure.smssecure.sms.MultipartSmsMessageHandler; import org.smssecure.smssecure.sms.OutgoingTextMessage; import org.smssecure.smssecure.transport.UndeliverableMessageException; +import org.smssecure.smssecure.util.dualsim.DualSimUtil; import org.smssecure.smssecure.util.NumberUtil; import org.smssecure.smssecure.util.SilencePreferences; import org.whispersystems.jobqueue.JobParameters; @@ -82,18 +83,9 @@ public class SmsSendJob extends SendJob { private void deliver(MasterSecret masterSecret, SmsMessageRecord message) throws UndeliverableMessageException - { - if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) { - deliverSecureMessage(masterSecret, message); - } else { - deliverPlaintextMessage(message); - } - } - - private void deliverSecureMessage(MasterSecret masterSecret, SmsMessageRecord message) - throws UndeliverableMessageException { String recipient = message.getIndividualRecipient().getNumber(); + ArrayList messages; // See issue #1516 for bug report, and discussion on commits related to #4833 for problems // related to the original fix to #1516. This still may not be a correct fix if networks allow @@ -103,74 +95,42 @@ public class SmsSendJob extends SendJob { recipient = PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(recipient)); } - MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler(); - OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message); - - if (message.isSecure() || message.isEndSession()) { - transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage); + if (!NumberUtil.isValidSmsOrEmail(recipient)) { + throw new UndeliverableMessageException("Not a valid SMS destination! " + recipient); } - ArrayList messages = SmsManager.getDefault().divideMessage(multipartMessageHandler.getEncodedMessage(transportMessage)); - ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure()); - ArrayList deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages); - - Log.w("SmsTransport", "Secure divide into message parts: " + messages.size()); - - try { - getSmsManagerFor(message.getSubscriptionId()).sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents); - } catch (NullPointerException npe) { - Log.w(TAG, npe); - Log.w(TAG, "Recipient: " + recipient); - Log.w(TAG, "Message Parts: " + messages.size()); - throw new UndeliverableMessageException(npe); - } catch (IllegalArgumentException iae) { - Log.w(TAG, iae); - throw new UndeliverableMessageException(iae); - } - } - - private void deliverPlaintextMessage(SmsMessageRecord message) - throws UndeliverableMessageException - { - String recipient = message.getIndividualRecipient().getNumber(); + if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) { + MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler(); + OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message); - // See issue #1516 for bug report, and discussion on commits related to #4833 for problems - // related to the original fix to #1516. This still may not be a correct fix if networks allow - // SMS/MMS sending to alphanumeric recipients other than email addresses, but should also - // help to fix issue #3099. - if (!NumberUtil.isValidEmail(recipient)) { - recipient = PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(recipient)); - } + if (!message.isKeyExchange()) { + transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage); + } - if (!NumberUtil.isValidSmsOrEmail(recipient)) { - throw new UndeliverableMessageException("Not a valid SMS destination! " + recipient); + messages = SmsManager.getDefault().divideMessage(multipartMessageHandler.getEncodedMessage(transportMessage)); + } else { + messages = SmsManager.getDefault().divideMessage(message.getBody().getBody()); } - ArrayList messages = SmsManager.getDefault().divideMessage(message.getBody().getBody()); - ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false); + ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure()); ArrayList deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages); + int deviceSubscriptionId = DualSimUtil.getSubscriptionIdFromAppSubscriptionId(context, message.getSubscriptionId()); + // NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients // and messages, this will throw an NPE. We have no idea why, so we're just // catching it and marking the message as a failure. That way at least it doesn't // repeatedly crash every time you start the app. try { - getSmsManagerFor(message.getSubscriptionId()).sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents); + getSmsManagerFor(deviceSubscriptionId).sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents); } catch (NullPointerException npe) { Log.w(TAG, npe); Log.w(TAG, "Recipient: " + recipient); Log.w(TAG, "Message Parts: " + messages.size()); - - try { - for (int i=0;i= 22 && subscriptionId != -1) { return SmsManager.getSmsManagerForSubscriptionId(subscriptionId); } else { diff --git a/src/org/smssecure/smssecure/jobs/SmsSentJob.java b/src/org/smssecure/smssecure/jobs/SmsSentJob.java index b04c5c2eb6ba6fb8f820fd8a066a331ecdfe410e..24b0bfcc7551a6f91919e851bdc24dba426d904d 100644 --- a/src/org/smssecure/smssecure/jobs/SmsSentJob.java +++ b/src/org/smssecure/smssecure/jobs/SmsSentJob.java @@ -94,7 +94,7 @@ public class SmsSentJob extends MasterSecretJob { if (record != null && record.isEndSession()) { Log.w(TAG, "Ending session..."); - SessionStore sessionStore = new SilenceSessionStore(context, masterSecret); + SessionStore sessionStore = new SilenceSessionStore(context, masterSecret, record.getSubscriptionId()); sessionStore.deleteAllSessions(record.getIndividualRecipient().getNumber()); SecurityEvent.broadcastSecurityUpdateEvent(context, record.getThreadId()); } diff --git a/src/org/smssecure/smssecure/notifications/AndroidAutoHeardReceiver.java b/src/org/smssecure/smssecure/notifications/AndroidAutoHeardReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..0ff6025eadd2733aaf4e6a55e990d86d66a39741 --- /dev/null +++ b/src/org/smssecure/smssecure/notifications/AndroidAutoHeardReceiver.java @@ -0,0 +1,71 @@ +/** + * 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.smssecure.smssecure.notifications; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationManagerCompat; +import android.util.Log; + +import org.smssecure.smssecure.crypto.MasterSecret; +import org.smssecure.smssecure.database.DatabaseFactory; + +import java.util.LinkedList; +import java.util.List; + +/** + * Marks an Android Auto as read after the driver have listened to it + */ +public class AndroidAutoHeardReceiver extends MasterSecretBroadcastReceiver { + + public static final String TAG = AndroidAutoHeardReceiver.class.getSimpleName(); + public static final String HEARD_ACTION = "org.smssecure.smssecure.notifications.ANDROID_AUTO_HEARD"; + public static final String THREAD_IDS_EXTRA = "car_heard_thread_ids"; + public static final String NOTIFICATION_ID_EXTRA = "car_notification_id"; + + @Override + protected void onReceive(final Context context, Intent intent, + @Nullable final MasterSecret masterSecret) + { + if (!HEARD_ACTION.equals(intent.getAction())) + return; + + final long[] threadIds = intent.getLongArrayExtra(THREAD_IDS_EXTRA); + + if (threadIds != null) { + int notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1); + NotificationManagerCompat.from(context).cancel(notificationId); + + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + for (long threadId : threadIds) { + Log.i(TAG, "Marking message as read: " + threadId); + DatabaseFactory.getThreadDatabase(context).setRead(threadId); + //DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId); + } + + MessageNotifier.updateNotification(context, masterSecret); + return null; + } + }.execute(); + } + } +} diff --git a/src/org/smssecure/smssecure/notifications/AndroidAutoReplyReceiver.java b/src/org/smssecure/smssecure/notifications/AndroidAutoReplyReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..cb49e85e1bcd29744e8265b2e7f09d9156e76114 --- /dev/null +++ b/src/org/smssecure/smssecure/notifications/AndroidAutoReplyReceiver.java @@ -0,0 +1,118 @@ +/** + * 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.smssecure.smssecure.notifications; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.RemoteInput; +import android.util.Log; + +import org.smssecure.smssecure.attachments.Attachment; +import org.smssecure.smssecure.crypto.MasterSecret; +import org.smssecure.smssecure.crypto.SessionUtil; +import org.smssecure.smssecure.database.DatabaseFactory; +import org.smssecure.smssecure.database.MessagingDatabase; +import org.smssecure.smssecure.database.RecipientPreferenceDatabase.RecipientsPreferences; +import org.smssecure.smssecure.mms.OutgoingMediaMessage; +import org.smssecure.smssecure.recipients.RecipientFactory; +import org.smssecure.smssecure.recipients.Recipients; +import org.smssecure.smssecure.sms.MessageSender; +import org.smssecure.smssecure.sms.OutgoingEncryptedMessage; +import org.smssecure.smssecure.sms.OutgoingTextMessage; +import org.whispersystems.libsignal.util.guava.Optional; + +import java.util.LinkedList; +import java.util.List; + +/** + * Get the response text from the Android Auto and sends an message as a reply + */ +public class AndroidAutoReplyReceiver extends MasterSecretBroadcastReceiver { + + public static final String TAG = AndroidAutoReplyReceiver.class.getSimpleName(); + public static final String REPLY_ACTION = "org.smssecure.smssecure.notifications.ANDROID_AUTO_REPLY"; + public static final String RECIPIENT_IDS_EXTRA = "car_recipient_ids"; + public static final String VOICE_REPLY_KEY = "car_voice_reply_key"; + public static final String THREAD_ID_EXTRA = "car_reply_thread_id"; + + @Override + protected void onReceive(final Context context, Intent intent, + final @Nullable MasterSecret masterSecret) + { + if (!REPLY_ACTION.equals(intent.getAction())) return; + + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + + if (remoteInput == null) return; + + final long[] recipientIds = intent.getLongArrayExtra(RECIPIENT_IDS_EXTRA); + final long threadId = intent.getLongExtra(THREAD_ID_EXTRA, -1); + final CharSequence responseText = getMessageText(intent); + final Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientIds, false); + + if (responseText != null) { + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + + long replyThreadId; + + Optional preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(recipientIds); + int subscriptionId = preferences.isPresent() ? preferences.get().getDefaultSubscriptionId().or(-1) : -1; + + if (recipients.isGroupRecipient()) { + Log.i(TAG, "GroupRecipient, Sending media message"); + OutgoingMediaMessage reply = new OutgoingMediaMessage(recipients, responseText.toString(), new LinkedList(), System.currentTimeMillis(), subscriptionId, 0); + replyThreadId = MessageSender.send(context, masterSecret, reply, threadId, false); + } else { + Log.i(TAG, "Sending regular message"); + boolean secure = SessionUtil.hasSession(context, masterSecret, recipients.getPrimaryRecipient().getNumber(), subscriptionId); + + OutgoingTextMessage reply; + if (!secure) { + reply = new OutgoingTextMessage(recipients, responseText.toString(), subscriptionId); + } else { + reply = new OutgoingEncryptedMessage(recipients, responseText.toString(), subscriptionId); + } + + replyThreadId = MessageSender.send(context, masterSecret, reply, threadId, false); + } + + DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId); + //DatabaseFactory.getThreadDatabase(context).setLastSeen(replyThreadId); + MessageNotifier.updateNotification(context, masterSecret); + + return null; + } + }.execute(); + } + } + + private CharSequence getMessageText(Intent intent) { + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + return remoteInput.getCharSequence(VOICE_REPLY_KEY); + } + return null; + } + +} diff --git a/src/org/smssecure/smssecure/notifications/MessageNotifier.java b/src/org/smssecure/smssecure/notifications/MessageNotifier.java index df873880253d34cd017a4b32da1f7619647f3d70..ad6284eba501a10c14f4f309b69ab60341e10e26 100644 --- a/src/org/smssecure/smssecure/notifications/MessageNotifier.java +++ b/src/org/smssecure/smssecure/notifications/MessageNotifier.java @@ -296,6 +296,8 @@ public class MessageNotifier { notificationState.getMarkAsReadIntent(context, notificationId), notificationState.getQuickReplyIntent(context, notifications.get(0).getRecipients()), notificationState.getRemoteReplyIntent(context, notifications.get(0).getRecipients())); + builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, notifications.get(0).getRecipients()), + notificationState.getAndroidAutoHeardIntent(context, notificationId), notifications.get(0).getTimestamp()); ListIterator iterator = notifications.listIterator(notifications.size()); diff --git a/src/org/smssecure/smssecure/notifications/NotificationState.java b/src/org/smssecure/smssecure/notifications/NotificationState.java index e2c6b99acd688b7d23661472f1d810f20b19a4a2..bf553bd7cfccee3dba6fc1f7171af0ca69a4ccac 100644 --- a/src/org/smssecure/smssecure/notifications/NotificationState.java +++ b/src/org/smssecure/smssecure/notifications/NotificationState.java @@ -127,6 +127,39 @@ public class NotificationState { return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } + public PendingIntent getAndroidAutoReplyIntent(Context context, Recipients recipients) { + if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications!"); + + Intent intent = new Intent(AndroidAutoReplyReceiver.REPLY_ACTION); + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); + intent.setClass(context, AndroidAutoReplyReceiver.class); + intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); + intent.putExtra(AndroidAutoReplyReceiver.RECIPIENT_IDS_EXTRA, recipients.getIds()); + intent.putExtra(AndroidAutoReplyReceiver.THREAD_ID_EXTRA, (long)threads.toArray()[0]); + intent.setPackage(context.getPackageName()); + + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + public PendingIntent getAndroidAutoHeardIntent(Context context, int notificationId) { + long[] threadArray = new long[threads.size()]; + int index = 0; + for (long thread : threads) { + Log.w("NotificationState", "getAndroidAutoHeardIntent Added thread: " + thread); + threadArray[index++] = thread; + } + + Intent intent = new Intent(AndroidAutoHeardReceiver.HEARD_ACTION); + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); + intent.setClass(context, AndroidAutoHeardReceiver.class); + intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); + intent.putExtra(AndroidAutoHeardReceiver.THREAD_IDS_EXTRA, threadArray); + intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId); + intent.setPackage(context.getPackageName()); + + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + public PendingIntent getQuickReplyIntent(Context context, Recipients recipients) { if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications! " + threads.size()); diff --git a/src/org/smssecure/smssecure/notifications/RemoteReplyReceiver.java b/src/org/smssecure/smssecure/notifications/RemoteReplyReceiver.java index d44eb62ce268f4d2f0f4d4b478523ce6f227d590..d94d118b3b6f0fede43df631fab717c695895acd 100644 --- a/src/org/smssecure/smssecure/notifications/RemoteReplyReceiver.java +++ b/src/org/smssecure/smssecure/notifications/RemoteReplyReceiver.java @@ -75,7 +75,7 @@ public class RemoteReplyReceiver extends MasterSecretBroadcastReceiver { OutgoingMediaMessage reply = new OutgoingMediaMessage(recipients, responseText.toString(), new LinkedList(), System.currentTimeMillis(), subscriptionId, 0); threadId = MessageSender.send(context, masterSecret, reply, -1, false); } else { - boolean secure = SessionUtil.hasSession(context, masterSecret, recipients.getPrimaryRecipient()); + boolean secure = SessionUtil.hasSession(context, masterSecret, recipients.getPrimaryRecipient().getNumber(), subscriptionId); OutgoingTextMessage reply; if (!secure) { diff --git a/src/org/smssecure/smssecure/notifications/SingleRecipientNotificationBuilder.java b/src/org/smssecure/smssecure/notifications/SingleRecipientNotificationBuilder.java index 8f4d0e0eacd5c339b7d6ab5841452133ffcbc9bd..1e103ea2e1444893e6320a8a17c847d417fe8e63 100644 --- a/src/org/smssecure/smssecure/notifications/SingleRecipientNotificationBuilder.java +++ b/src/org/smssecure/smssecure/notifications/SingleRecipientNotificationBuilder.java @@ -99,6 +99,27 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil } } + public void addAndroidAutoAction(@NonNull PendingIntent androidAutoReplyIntent, + @NonNull PendingIntent androidAutoHeardIntent, long timestamp) + { + + if (mContentTitle == null || mContentText == null) + return; + + RemoteInput remoteInput = new RemoteInput.Builder(AndroidAutoReplyReceiver.VOICE_REPLY_KEY) + .setLabel(context.getString(R.string.MessageNotifier_reply)) + .build(); + + NotificationCompat.CarExtender.UnreadConversation.Builder unreadConversationBuilder = + new NotificationCompat.CarExtender.UnreadConversation.Builder(mContentTitle.toString()) + .addMessage(mContentText.toString()) + .setLatestTimestamp(timestamp) + .setReadPendingIntent(androidAutoHeardIntent) + .setReplyAction(androidAutoReplyIntent, remoteInput); + + extend(new NotificationCompat.CarExtender().setUnreadConversation(unreadConversationBuilder.build())); + } + public void addActions(@Nullable MasterSecret masterSecret, @NonNull PendingIntent markReadIntent, @NonNull PendingIntent quickReplyIntent, diff --git a/src/org/smssecure/smssecure/protocol/AutoInitiate.java b/src/org/smssecure/smssecure/protocol/AutoInitiate.java index b0063be90fd8851445713aaa643f6c8ac7615fc2..be6cf1986071550eb4ecc537877149d907e7b336 100644 --- a/src/org/smssecure/smssecure/protocol/AutoInitiate.java +++ b/src/org/smssecure/smssecure/protocol/AutoInitiate.java @@ -14,6 +14,7 @@ import org.smssecure.smssecure.crypto.MasterSecret; import org.smssecure.smssecure.crypto.SessionUtil; import org.smssecure.smssecure.recipients.Recipient; import org.smssecure.smssecure.recipients.Recipients; +import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat; import java.util.Locale; @@ -89,7 +90,7 @@ public class AutoInitiate { MasterSecret masterSecret, Recipient recipient) { - return !SessionUtil.hasSession(context, masterSecret, recipient); + return !SessionUtil.hasSession(context, masterSecret, recipient.getNumber(), SubscriptionManagerCompat.from(context).getActiveSubscriptionInfoList()); } } diff --git a/src/org/smssecure/smssecure/recipients/Recipients.java b/src/org/smssecure/smssecure/recipients/Recipients.java index 06d68faeb94a5bd9cc60922230dd58e907a9aad8..da4b2437bf254ef559384922d1a2f5f83d321aec 100644 --- a/src/org/smssecure/smssecure/recipients/Recipients.java +++ b/src/org/smssecure/smssecure/recipients/Recipients.java @@ -50,11 +50,12 @@ public class Recipients implements Iterable, RecipientModifiedListene private final Set listeners = Collections.newSetFromMap(new WeakHashMap()); private final List recipients; - private Uri ringtone = null; - private long mutedUntil = 0; - private boolean blocked = false; - private VibrateState vibrate = VibrateState.DEFAULT; - private boolean stale = false; + private Uri ringtone = null; + private long mutedUntil = 0; + private boolean blocked = false; + private VibrateState vibrate = VibrateState.DEFAULT; + private boolean stale = false; + private int defaultSubscriptionId = -1; Recipients() { this(new LinkedList(), null); @@ -64,10 +65,11 @@ public class Recipients implements Iterable, RecipientModifiedListene this.recipients = recipients; if (preferences != null) { - ringtone = preferences.getRingtone(); - mutedUntil = preferences.getMuteUntil(); - vibrate = preferences.getVibrateState(); - blocked = preferences.isBlocked(); + ringtone = preferences.getRingtone(); + mutedUntil = preferences.getMuteUntil(); + vibrate = preferences.getVibrateState(); + blocked = preferences.isBlocked(); + defaultSubscriptionId = preferences.getDefaultSubscriptionId().or(-1); } } @@ -78,10 +80,11 @@ public class Recipients implements Iterable, RecipientModifiedListene this.recipients = recipients; if (stale != null) { - ringtone = stale.ringtone; - mutedUntil = stale.mutedUntil; - vibrate = stale.vibrate; - blocked = stale.blocked; + ringtone = stale.ringtone; + mutedUntil = stale.mutedUntil; + vibrate = stale.vibrate; + blocked = stale.blocked; + defaultSubscriptionId = stale.defaultSubscriptionId; } preferences.addListener(new FutureTaskListener() { @@ -92,10 +95,11 @@ public class Recipients implements Iterable, RecipientModifiedListene Set localListeners; synchronized (Recipients.this) { - ringtone = result.getRingtone(); - mutedUntil = result.getMuteUntil(); - vibrate = result.getVibrateState(); - blocked = result.isBlocked(); + ringtone = result.getRingtone(); + mutedUntil = result.getMuteUntil(); + vibrate = result.getVibrateState(); + blocked = result.isBlocked(); + defaultSubscriptionId = result.getDefaultSubscriptionId().or(-1); localListeners = new HashSet<>(listeners); } @@ -161,6 +165,18 @@ public class Recipients implements Iterable, RecipientModifiedListene notifyListeners(); } + public synchronized int getDefaultSubscriptionId() { + return defaultSubscriptionId; + } + + public void setDefaultSubscriptionId(int defaultSubscriptionId) { + synchronized (this) { + this.defaultSubscriptionId = defaultSubscriptionId; + } + + notifyListeners(); + } + public @NonNull ContactPhoto getContactPhoto() { if (recipients.size() == 1) return recipients.get(0).getContactPhoto(); else return ContactPhotoFactory.getDefaultGroupPhoto(); diff --git a/src/org/smssecure/smssecure/sms/IncomingTextMessage.java b/src/org/smssecure/smssecure/sms/IncomingTextMessage.java index efe5df40d4ae8ddc2dbc3632b5cfdb60c06cbf5b..68603b7e07dc8c799755193cba45d8d0f30043cf 100644 --- a/src/org/smssecure/smssecure/sms/IncomingTextMessage.java +++ b/src/org/smssecure/smssecure/sms/IncomingTextMessage.java @@ -54,7 +54,7 @@ public class IncomingTextMessage implements Parcelable { this.receivedWhenLocked = receivedWhenLocked; } - public IncomingTextMessage(String sender, int senderDeviceId, long sentTimestampMillis, String encodedBody) { + public IncomingTextMessage(String sender, int senderDeviceId, long sentTimestampMillis, String encodedBody, int subscriptionId) { this.message = encodedBody; this.sender = sender; this.senderDeviceId = senderDeviceId; @@ -64,8 +64,8 @@ public class IncomingTextMessage implements Parcelable { this.pseudoSubject = ""; this.sentTimestampMillis = sentTimestampMillis; this.push = true; - this.subscriptionId = -1; - this.groupId = null; + this.subscriptionId = subscriptionId; + this.groupId = null; this.receivedWhenLocked = false; } diff --git a/src/org/smssecure/smssecure/sms/MessageSender.java b/src/org/smssecure/smssecure/sms/MessageSender.java index ec0ec156ca19cab08e2d848e86f32ecafbbc7edf..37acb33ebba90af65231f460567447cf0427a6d0 100644 --- a/src/org/smssecure/smssecure/sms/MessageSender.java +++ b/src/org/smssecure/smssecure/sms/MessageSender.java @@ -55,7 +55,6 @@ public class MessageSender { { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); Recipients recipients = message.getRecipients(); - boolean keyExchange = message.isKeyExchange(); long allocatedThreadId; diff --git a/src/org/smssecure/smssecure/util/DummyCharacterCalculator.java b/src/org/smssecure/smssecure/util/DummyCharacterCalculator.java new file mode 100644 index 0000000000000000000000000000000000000000..7921f475062fd2cf74944133e74af4901f9858d3 --- /dev/null +++ b/src/org/smssecure/smssecure/util/DummyCharacterCalculator.java @@ -0,0 +1,9 @@ +package org.smssecure.smssecure.util; + +public class DummyCharacterCalculator extends CharacterCalculator { + + @Override + public CharacterState calculateCharacters(String messageBody) { + return new CharacterState(0, 0, 0); + } +} diff --git a/src/org/smssecure/smssecure/util/SilencePreferences.java b/src/org/smssecure/smssecure/util/SilencePreferences.java index ed04c9f2757566ad0ed453bc3f8ff21982674a63..6ccf2e2bb68d5b908b1116ba1d3fa5fa082c4ced 100644 --- a/src/org/smssecure/smssecure/util/SilencePreferences.java +++ b/src/org/smssecure/smssecure/util/SilencePreferences.java @@ -97,9 +97,14 @@ public class SilencePreferences { private static final String MEDIA_DOWNLOAD_ROAMING_PREF = "pref_media_download_roaming"; public static final String SYSTEM_EMOJI_PREF = "pref_system_emoji"; - public static final String INCOGNITO_KEYBORAD_PREF = "pref_incognito_keyboard"; + private static final String APP_SUBSCRIPTION_ID_FOR_DEVICE_SUBSCRIPTION_ID_PREF = "app_subscription_id_for_device_subscription_id"; + private static final String LAST_APP_SUBSCRIPTION_ID_PREF = "last_app_subscription_id"; + private static final String NUMBER_FOR_APP_SUBSCRIPTION_ID_PREF = "number_for_app_subscription_id"; + private static final String ICC_ID_FOR_APP_SUBSCRIPTION_ID_PREF = "icc_id_for_app_subscription_id"; + private static final String SUBSCRIPTIONS_PREF = "pref_subscriptions"; + public static boolean isIncognitoKeyboardEnabled(Context context) { return getBooleanPreference(context, INCOGNITO_KEYBORAD_PREF, true); } @@ -159,7 +164,7 @@ public class SilencePreferences { public static void setGcmRegistrationId(Context context, String registrationId) { setStringPreference(context, GCM_REGISTRATION_ID_PREF, registrationId); - setIntegerPrefrence(context, GCM_REGISTRATION_ID_VERSION_PREF, Util.getCurrentApkReleaseVersion(context)); + setIntegerPreference(context, GCM_REGISTRATION_ID_VERSION_PREF, Util.getCurrentApkReleaseVersion(context)); } public static String getGcmRegistrationId(Context context) { @@ -217,7 +222,7 @@ public class SilencePreferences { } public static void setLocalRegistrationId(Context context, int registrationId) { - setIntegerPrefrence(context, LOCAL_REGISTRATION_ID_PREF, registrationId); + setIntegerPreference(context, LOCAL_REGISTRATION_ID_PREF, registrationId); } public static boolean isInThreadNotifications(Context context) { @@ -385,7 +390,7 @@ public class SilencePreferences { } public static void setLastVersionCode(Context context, int versionCode) throws IOException { - if (!setIntegerPrefrenceBlocking(context, LAST_VERSION_CODE_PREF, versionCode)) { + if (!setIntegerPreferenceBlocking(context, LAST_VERSION_CODE_PREF, versionCode)) { throw new IOException("couldn't write version code to sharedpreferences"); } } @@ -436,7 +441,7 @@ public class SilencePreferences { } public static void setPassphraseTimeoutInterval(Context context, int interval) { - setIntegerPrefrence(context, PASSPHRASE_TIMEOUT_INTERVAL_PREF, interval); + setIntegerPreference(context, PASSPHRASE_TIMEOUT_INTERVAL_PREF, interval); } public static String getLanguage(Context context) { @@ -559,6 +564,46 @@ public class SilencePreferences { return getBooleanPreference(context, HIDE_UNREAD_MESSAGE_DIVIDER, false); } + public static int getLastAppSubscriptionId(Context context) { + return getIntegerPreference(context, LAST_APP_SUBSCRIPTION_ID_PREF, 0); + } + + public static void setLastAppSubscriptionId(Context context, int appSubscriptionId) { + setIntegerPreference(context, LAST_APP_SUBSCRIPTION_ID_PREF, appSubscriptionId); + } + public static int getAppSubscriptionId(Context context, int deviceSubscriptionId) { + return getIntegerPreference(context, APP_SUBSCRIPTION_ID_FOR_DEVICE_SUBSCRIPTION_ID_PREF + "_" + deviceSubscriptionId, -1); + } + + public static void setAppSubscriptionId(Context context, int deviceSubscriptionId, int appSubscriptionId) { + setIntegerPreference(context, APP_SUBSCRIPTION_ID_FOR_DEVICE_SUBSCRIPTION_ID_PREF + "_" + appSubscriptionId, deviceSubscriptionId); + if (appSubscriptionId > getLastAppSubscriptionId(context)) setLastAppSubscriptionId(context, appSubscriptionId); + } + + public static void setNumberForSubscriptionId(Context context, int subscriptionId, String number) { + setStringPreference(context, NUMBER_FOR_APP_SUBSCRIPTION_ID_PREF + "_" + subscriptionId, number); + } + + public static String getNumberForSubscriptionId(Context context, int subscriptionId) { + return getStringPreference(context, NUMBER_FOR_APP_SUBSCRIPTION_ID_PREF + "_" + subscriptionId, null); + } + + public static void setIccIdForSubscriptionId(Context context, int subscriptionId, String iccId) { + setStringPreference(context, ICC_ID_FOR_APP_SUBSCRIPTION_ID_PREF + "_" + subscriptionId, iccId); + } + + public static String getIccIdForSubscriptionId(Context context, int subscriptionId) { + return getStringPreference(context, ICC_ID_FOR_APP_SUBSCRIPTION_ID_PREF + "_" + subscriptionId, null); + } + + public static void setDeviceSubscriptions(Context context, String subscriptions) { + setStringPreference(context, SUBSCRIPTIONS_PREF, subscriptions); + } + + public static String getDeviceSubscriptions(Context context) { + return getStringPreference(context, SUBSCRIPTIONS_PREF, ""); + } + public static void setBooleanPreference(Context context, String key, boolean value) { PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply(); } @@ -579,11 +624,11 @@ public class SilencePreferences { return PreferenceManager.getDefaultSharedPreferences(context).getInt(key, defaultValue); } - private static void setIntegerPrefrence(Context context, String key, int value) { + private static void setIntegerPreference(Context context, String key, int value) { PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(key, value).apply(); } - private static boolean setIntegerPrefrenceBlocking(Context context, String key, int value) { + private static boolean setIntegerPreferenceBlocking(Context context, String key, int value) { return PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(key, value).commit(); } diff --git a/src/org/smssecure/smssecure/util/VersionTracker.java b/src/org/smssecure/smssecure/util/VersionTracker.java index 8c45a32f005a790da8011a345501ebbac301d428..f9816313eb09c79201757154e3b0ded8ce35ccc8 100644 --- a/src/org/smssecure/smssecure/util/VersionTracker.java +++ b/src/org/smssecure/smssecure/util/VersionTracker.java @@ -1,12 +1,13 @@ package org.smssecure.smssecure.util; import android.content.Context; -import android.content.pm.PackageManager; +import android.content.pm.PackageInfo; +import android.util.Log; import java.io.IOException; public class VersionTracker { - + private static final String TAG = VersionTracker.class.getSimpleName(); public static int getLastSeenVersion(Context context) { return SilencePreferences.getLastVersionCode(context); @@ -20,4 +21,16 @@ public class VersionTracker { throw new AssertionError(ioe); } } + + public static boolean isDbUpdated(Context context) { + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + if (packageInfo == null) return true; + + return SilencePreferences.getLastVersionCode(context) >= packageInfo.versionCode; + } catch (Exception e) { + Log.w(TAG, e); + return true; + } + } } diff --git a/src/org/smssecure/smssecure/util/dualsim/DualSimUtil.java b/src/org/smssecure/smssecure/util/dualsim/DualSimUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..a67701fb1fa9b9374bab030e93cd632c0bfdfcbc --- /dev/null +++ b/src/org/smssecure/smssecure/util/dualsim/DualSimUtil.java @@ -0,0 +1,131 @@ +package org.smssecure.smssecure.util.dualsim; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.support.v4.app.NotificationCompat; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.util.Log; + +import org.smssecure.smssecure.crypto.IdentityKeyUtil; +import org.smssecure.smssecure.crypto.MasterSecret; +import org.smssecure.smssecure.crypto.MasterSecretUtil; +import org.smssecure.smssecure.crypto.storage.SilenceSessionStore; +import org.smssecure.smssecure.R; +import org.smssecure.smssecure.util.ServiceUtil; +import org.smssecure.smssecure.util.SilencePreferences; +import org.whispersystems.libsignal.util.guava.Optional; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; + +public class DualSimUtil { + private static final String TAG = DualSimUtil.class.getSimpleName(); + + private static final int NOTIFICATION_ID = 1340; + + public static void moveIdentityKeysAndSessionsToSubscriptionId(Context context, int originalSubscriptionId, int subscriptionId) { + Log.w(TAG, "moveIdentityKeysMasterSecretAndSessionsToSubscriptionId(" + originalSubscriptionId + ", " + subscriptionId + ")"); + + moveIdentityKeysToSubscriptionId(context, originalSubscriptionId, subscriptionId); + moveSessionsToSubscriptionId(context, originalSubscriptionId, subscriptionId); + } + + private static void moveIdentityKeysToSubscriptionId(Context context, int originalSubscriptionId, int subscriptionId) { + String originalIdentityPublicPref = IdentityKeyUtil.getIdentityPublicKeyDjbPref(originalSubscriptionId); + String identityPublicPref = IdentityKeyUtil.getIdentityPublicKeyDjbPref(subscriptionId); + String originalIdentityPrivatePref = IdentityKeyUtil.getIdentityPrivateKeyDjbPref(originalSubscriptionId); + String identityPrivatePref = IdentityKeyUtil.getIdentityPrivateKeyDjbPref(subscriptionId); + + Log.w(TAG, "Moving " + originalIdentityPublicPref + " to " + identityPublicPref); + Log.w(TAG, "Moving " + originalIdentityPrivatePref + " to " + identityPrivatePref); + + String identityPublicKey = IdentityKeyUtil.retrieve(context, originalIdentityPublicPref); + String identityPrivateKey = IdentityKeyUtil.retrieve(context, originalIdentityPrivatePref); + + IdentityKeyUtil.save(context, identityPublicPref, identityPublicKey); + IdentityKeyUtil.save(context, identityPrivatePref, identityPrivateKey); + + IdentityKeyUtil.remove(context, originalIdentityPublicPref); + IdentityKeyUtil.remove(context, originalIdentityPrivatePref); + } + + private static void moveSessionsToSubscriptionId(Context context, int originalSubscriptionId, int subscriptionId) { + File sessionDirectory = SilenceSessionStore.getSessionDirectory(context); + + File[] sessionList = sessionDirectory.listFiles(); + + String destinationSuffix = subscriptionId != -1 ? "." + subscriptionId : ""; + + for (File session : sessionList){ + if (session.isFile()){ + String absolutePath = session.getAbsolutePath(); + String newSessionName = null; + + if (originalSubscriptionId != -1 && absolutePath.endsWith("." + originalSubscriptionId)) { + newSessionName = absolutePath.replaceAll("/\\." + originalSubscriptionId + "/g", destinationSuffix); + } else if (originalSubscriptionId == -1) { + newSessionName = absolutePath + destinationSuffix; + } + + if (newSessionName != null) { + Log.w(TAG, "Moving session " + absolutePath + " to " + newSessionName); + File newFile = new File(newSessionName); + if (session.renameTo(newFile)) { + Log.w(TAG, "Done!"); + } else { + Log.w(TAG, "Failed!"); + } + } + + } + } + } + + public static void generateKeysIfDoNotExist(Context context, MasterSecret masterSecret, List activeSubscriptions) { + generateKeysIfDoNotExist(context, masterSecret, activeSubscriptions, true); + } + + public static void generateKeysIfDoNotExist(Context context, MasterSecret masterSecret, List activeSubscriptions, boolean displayNotification) { + for (SubscriptionInfoCompat subscriptionInfo : activeSubscriptions) { + int subscriptionId = subscriptionInfo.getSubscriptionId(); + + if (!IdentityKeyUtil.hasIdentityKey(context, subscriptionId)) + IdentityKeyUtil.generateIdentityKeys(context, masterSecret, subscriptionId, displayNotification); + } + } + + public static int getSubscriptionIdFromAppSubscriptionId(Context context, int appSubscriptionId) { + Optional subscriptionInfo = SubscriptionManagerCompat.from(context).getActiveSubscriptionInfo(appSubscriptionId); + if (subscriptionInfo.isPresent()) return subscriptionInfo.get().getDeviceSubscriptionId(); + else return -1; + } + + public static int getSubscriptionIdFromDeviceSubscriptionId(Context context, int deviceSubscriptionId) { + Optional subscriptionInfo = SubscriptionManagerCompat.from(context).getActiveSubscriptionInfoFromDeviceSubscriptionId(deviceSubscriptionId); + if (subscriptionInfo.isPresent()) return subscriptionInfo.get().getSubscriptionId(); + else return -1; + } + + public static void displayNotification(Context context) { + Intent targetIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); + Notification notification = new NotificationCompat.Builder(context) + .setSmallIcon(R.drawable.icon_notification) + .setColor(context.getResources().getColor(R.color.silence_primary)) + .setContentTitle(context.getString(R.string.DualSimUtil__new_sim_card_detected)) + .setContentText(context.getString(R.string.DualSimUtil__a_new_key_has_been_generated)) + .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.DualSimUtil__a_new_key_has_been_generated_for_that_new_sim_card))) + .setAutoCancel(true) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setContentIntent(PendingIntent.getActivity(context.getApplicationContext(), 0, + targetIntent, + PendingIntent.FLAG_UPDATE_CURRENT)) + .build(); + ServiceUtil.getNotificationManager(context).notify(NOTIFICATION_ID, notification); + } +} diff --git a/src/org/smssecure/smssecure/util/dualsim/SimChangedReceiver.java b/src/org/smssecure/smssecure/util/dualsim/SimChangedReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..96ae380b7c48b02c5af0dd94ce5bfc43d2d9c499 --- /dev/null +++ b/src/org/smssecure/smssecure/util/dualsim/SimChangedReceiver.java @@ -0,0 +1,85 @@ +package org.smssecure.smssecure.util.dualsim; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.util.Log; + +import org.smssecure.smssecure.ApplicationContext; +import org.smssecure.smssecure.util.SilencePreferences; +import org.smssecure.smssecure.util.VersionTracker; +import org.smssecure.smssecure.jobs.GenerateKeysJob; + +import java.util.Arrays; +import java.util.List; + +public class SimChangedReceiver extends BroadcastReceiver { + private static final String TAG = SimChangedReceiver.class.getSimpleName(); + + private static final String SIM_STATE_CHANGED_EVENT = "android.intent.action.SIM_STATE_CHANGED"; + + @Override + public void onReceive(final Context context, final Intent intent) { + Log.w(TAG, "onReceive()"); + + if (intent.getAction().equals(SIM_STATE_CHANGED_EVENT)) { + checkSimState(context); + } + } + + public static void checkSimState(final Context context) { + if (hasDifferentSubscriptions(context) && VersionTracker.isDbUpdated(context)) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new GenerateKeysJob(context)); + SilencePreferences.setDeviceSubscriptions(context, getDeviceSubscriptions(context)); + } + SubscriptionManagerCompat.from(context).updateActiveSubscriptionInfoList(); + } + + private static boolean hasDifferentSubscriptions(Context context) { + String subscriptions = getDeviceSubscriptions(context); + String registeredSubscriptions = getActiveDeviceSubscriptionIds(context); + + Log.w(TAG, "getDeviceSubscriptions(): " + getDeviceSubscriptions(context)); + Log.w(TAG, "getActiveDeviceSubscriptionIds(): " + getActiveDeviceSubscriptionIds(context)); + + return !subscriptions.equals(registeredSubscriptions); + } + + private static String getDeviceSubscriptions(Context context) { + if (Build.VERSION.SDK_INT < 22) return "1"; + + SubscriptionManager subscriptionManager = SubscriptionManager.from(context); + List activeSubscriptions = subscriptionManager.getActiveSubscriptionInfoList(); + + if (activeSubscriptions == null) return "1"; + + String[] subscriptions = new String[activeSubscriptions.size()]; + for(int i=0; i displayNameList; + private List compatList; - public SubscriptionManagerCompat(Context context) { + public static SubscriptionManagerCompat from(Context context) { + if (instance == null) { + instance = new SubscriptionManagerCompat(context); + } + return instance; + } + + private SubscriptionManagerCompat(Context context) { this.context = context.getApplicationContext(); + this.displayNameList = new LinkedList(); } public Optional getActiveSubscriptionInfo(int subscriptionId) { - if (Build.VERSION.SDK_INT < 22) { + if (getActiveSubscriptionInfoList().size() <= 0) { return Optional.absent(); } - SubscriptionInfo subscriptionInfo = SubscriptionManager.from(context).getActiveSubscriptionInfo(subscriptionId); + for (SubscriptionInfoCompat subscriptionInfo : getActiveSubscriptionInfoList()) { + if (subscriptionInfo.getSubscriptionId() == subscriptionId) return Optional.of(subscriptionInfo); + } - if (subscriptionInfo != null) { - return Optional.of(new SubscriptionInfoCompat(subscriptionId, subscriptionInfo.getDisplayName())); - } else { + return Optional.absent(); + } + + public Optional getActiveSubscriptionInfoFromDeviceSubscriptionId(int subscriptionId) { + if (getActiveSubscriptionInfoList().size() <= 0) { return Optional.absent(); } + + for (SubscriptionInfoCompat subscriptionInfo : getActiveSubscriptionInfoList()) { + if (subscriptionInfo.getDeviceSubscriptionId() == subscriptionId) return Optional.of(subscriptionInfo); + } + + return Optional.absent(); + } + + @TargetApi(22) + private void updateDisplayNameList(List activeSubscriptions) { + displayNameList = new LinkedList(); + + if (activeSubscriptions != null) { + for (SubscriptionInfo subscriptionInfo : activeSubscriptions) { + displayNameList.add(subscriptionInfo.getDisplayName().toString()); + } + } + } + + public boolean knowThisDisplayNameTwice(CharSequence displayName) { + if (displayName == null) return false; + + boolean found = false; + + for (String potentialDisplayName : displayNameList) { + if (found && potentialDisplayName != null && potentialDisplayName.equals(displayName.toString())) + return true; + if (potentialDisplayName != null && potentialDisplayName.equals(displayName.toString())) + found = true; + } + return false; } public @NonNull List getActiveSubscriptionInfoList() { + if (compatList == null) return updateActiveSubscriptionInfoList(); + return compatList; + } + + public @NonNull List updateActiveSubscriptionInfoList() { + compatList = new LinkedList<>(); + if (Build.VERSION.SDK_INT < 22) { - return new LinkedList<>(); + TelephonyManager telephonyManager = ServiceUtil.getTelephonyManager(context); + compatList.add(new SubscriptionInfoCompat(context, + -1, + telephonyManager.getSimOperatorName(), + telephonyManager.getLine1Number(), + telephonyManager.getSimSerialNumber(), + 1, + false)); + return compatList; } List subscriptionInfos = SubscriptionManager.from(context).getActiveSubscriptionInfoList(); + updateDisplayNameList(subscriptionInfos); if (subscriptionInfos == null || subscriptionInfos.isEmpty()) { - return new LinkedList<>(); + return compatList; } - List compatList = new LinkedList<>(); - for (SubscriptionInfo subscriptionInfo : subscriptionInfos) { - compatList.add(new SubscriptionInfoCompat(subscriptionInfo.getSubscriptionId(), - subscriptionInfo.getDisplayName())); + compatList.add(new SubscriptionInfoCompat(context, + subscriptionInfo.getSubscriptionId(), + subscriptionInfo.getDisplayName(), + subscriptionInfo.getNumber(), + subscriptionInfo.getIccId(), + subscriptionInfo.getSimSlotIndex()+1, + knowThisDisplayNameTwice(subscriptionInfo.getDisplayName()))); } return compatList;