diff --git a/.travis.yml b/.travis.yml
index 96ab6bdb88c69439206e5e4b2029cb57e8a94b32..22f96677d5e80fa26a295f4a17588bdabdf8e9f1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,6 +17,7 @@ android:
- build-tools-20.0.0
- build-tools-19.1.0
- android-22
+ - android-25
- extra-android-m2repository
before_script:
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a702cf216e80a377a473124dc9f7070bef976c3a..d77750d33f50e8ecd1e1b0bff81afd395b0779c4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -22,6 +22,7 @@
+
@@ -76,6 +77,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/register_xmpp_account.xml b/res/layout/register_xmpp_account.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b0ceda5754133b0f0932b009ef1e76d70428b970
--- /dev/null
+++ b/res/layout/register_xmpp_account.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/xmpp_address_dialog.xml b/res/layout/xmpp_address_dialog.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0e7e9edbb2112516a3675b7c53fa2238ce627b0e
--- /dev/null
+++ b/res/layout/xmpp_address_dialog.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/xmpp_enter_address_dialog.xml b/res/layout/xmpp_enter_address_dialog.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2211b94de9bcfba5871137ed5716041471943294
--- /dev/null
+++ b/res/layout/xmpp_enter_address_dialog.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/res/menu/conversation_secure_xmpp.xml b/res/menu/conversation_secure_xmpp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..64f160296b003c669e2780c3cce9d35d8f0b4799
--- /dev/null
+++ b/res/menu/conversation_secure_xmpp.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/res/values-v11/styles.xml b/res/values-v11/styles.xml
index 26fd14565ed6d0a6a68f1aa68f90b59ac1b8a5f2..87c12a7347149a4fba88128826ebbfeec95ef0f2 100644
--- a/res/values-v11/styles.xml
+++ b/res/values-v11/styles.xml
@@ -4,11 +4,11 @@
-
-
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 36beecfcb2c091eed90620fce671a6baa6b69962..b4afafac3a3146b4473902d34e9f7ba92abfbe35 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -66,6 +66,7 @@
+
@@ -112,6 +113,7 @@
+
diff --git a/res/values/colors.xml b/res/values/colors.xml
index ce85dc4227299676501562e1bb4e9ef495eab7e7..1f688c8c9fb995a4341b5729aa55447c367b4f94 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -4,6 +4,8 @@
#ff1c7ac5
#552090ea
#882090ea
+ #ff139f5b
+ #fff57f17
#ffffffff
#ff000000
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 44d0c377f872b70e9402e49b4466eed6aeb97a54..b9131737159b952e7cd22ad9e4b610c27b57263a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -83,7 +83,7 @@
Can\'t find an app able to open this media.
from %s
to %s
- XMPP address. Update Silence to enable this feature.
+ XMPP address
Initiate secure session?
@@ -117,12 +117,14 @@
Insecure MMS
Secure SMS
Secure MMS
+ XMPP
Let\'s chat securely: %s
Secure sessions can only be established with other people.
MMS not supported
This message cannot be sent since your carrier doesn\'t support MMS.
Please choose a contact
Attachment exceeds size limits.
+ Recipient is offline. XMPP messages may be delayed.
Message details
@@ -157,6 +159,7 @@
Pending...
MMS
SMS
+ XMPP
Deleting...
Deleting messages...
@@ -206,6 +209,8 @@
Share with
+ Add XMPP address
+ XMPP address added
Welcome to Silence!
@@ -216,6 +221,9 @@
Silence can use unencrypted messages to chat with non-Silence users.
SMSSecure is now Silence.
SMSSecure is now Silence. Your messages are still here and new features are coming.
+ Protect your metadata
+ You can optionally enable XMPP messages to protect your metadata.
+ Tap to configure XMPP messages
Export
@@ -429,6 +437,34 @@
Silence
New message
+
+ Cannot reach XMPP server
+ XMPP connection failed
+ XMPP features in Silence are disabled.
+
+
+ Done!
+
+
+ Share XMPP address
+ Silence XMPP address
+ XMPP address copied!
+ Share
+ Here is my Silence XMPP address: %1$s. Enter it into Silence to start a secure conversation.
+
+
+ Cannot create an XMPP account. Please try again later.
+ Registered!
+ Unregistered!
+ You are already registered!
+
+
+ XMPP address
+ Use this form to enter the XMPP address of %s.
+
+
+ Send the link above to your contacts to start using XMPP messages with them.
+
Old passphrase
New passphrase
@@ -461,6 +497,7 @@
Send unsecured SMS
Send secure MMS
Send unsecured MMS
+ Send secure XMPP message
From %1$s
Send
Remove
@@ -473,6 +510,7 @@
Media message downloading
Media message
Secure message
+ XMPP message
Download
@@ -582,6 +620,8 @@
Message details
Archived conversations
Configure MMS Settings
+ Register
+ Use a custom XMPP server
Import / export
@@ -720,6 +760,20 @@
Roaming auto-retrieve
Automatically retrieve MMS when roaming
Message trimming
+ XMPP messages
+ Enable XMPP messages
+ XMPP status
+ Registered, connected.
+ Registered, disconnected.
+ Unregistered.
+ Unregistering XMPP?
+ Unregistering from XMPP will delete your account on the server and notify every contact with an XMPP session using SMS messages.
+ Notify contacts
+ Send an SMS message with my XMPP address to every contact with a secure session.
+ Notifying
+ Sending messages to contacts
+ Force XMPP messages
+ Enable this option to always use XMPP transport even if recipient is offline.
@@ -784,7 +838,10 @@
Security
Verify identity
+ Share XMPP address
+ Share XMPP address via SMS
End secure session
+ Enter XMPP address manually
Unmute
@@ -862,6 +919,21 @@
Transport icon
+
+ You can enable XMPP messages in Silence to protect your metadata. Metadata (mostly to who you talk and when) is a sensitive information that cannot be hidden using SMS messages. XMPP messages use data so you need to be connected to the Internet to use this transport and receive messages.
+ Remember: there is one metadata that you cannot hide. Your GSM carrier will always be able to locate your phone at any time.
+ If you don\'t have an XMPP account, Silence will create one on a random trusted server. If you manage your own XMPP server, you can use it instead.
+ Use a trusted server
+ Use my own server
+ Cancel
+ Registration
+ Creating an XMPP account...
+ Hostname
+ Port
+ Connect
+ Invalid port.
+ Cannot register on this server.
+ It is highly recommended to enable the Stream Management extension (XEP-0198) on your server. Stream Management allows Silence to resume connections. Without that, you may lost messages on unreliable connections.
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 44245334a1f50326e88a65b744f5f434b2f194aa..49e59dd3d73915634b61d28a4a371901f05d1fed 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -140,6 +140,7 @@
- #99000000
- @color/white
- #BFffffff
+ - @drawable/ic_xmpp_white_18dp
- @drawable/conversation_item_sent_indicator_text_shape
@@ -182,6 +183,7 @@
- @drawable/ic_app_protection_black
- @drawable/ic_brightness_6_black
- @drawable/ic_forum_black_32dp
+ - @drawable/ic_xmpp_black
- @drawable/ic_advanced_black
- @style/BetterPickersDialogFragment.Light
@@ -220,6 +222,7 @@
- @color/white
- #BFffffff
- @drawable/conversation_item_sent_indicator_text_shape_dark
+ - @drawable/ic_xmpp_white_18dp
- @drawable/ic_info_outline_dark
- @drawable/ic_warning_dark
@@ -296,6 +299,7 @@
- @drawable/ic_app_protection_gray
- @drawable/ic_brightness_6_gray
- @drawable/ic_forum_grey_32dp
+ - @drawable/ic_xmpp_grey
- @drawable/ic_advanced_gray
- @style/BetterPickersDialogFragment
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 1d672575902b59f941a63fd176a4da715a9b58e9..f12ea854065b1b025bc12e0d28cf50814c68667b 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -21,6 +21,10 @@
android:title="@string/preferences__chats"
android:icon="?pref_ic_chats"/>
+
+
diff --git a/res/xml/preferences_xmpp.xml b/res/xml/preferences_xmpp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b50489546f848775ae9054fe4695d17ac9469a75
--- /dev/null
+++ b/res/xml/preferences_xmpp.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/xml/provider_paths.xml b/res/xml/provider_paths.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4ff89311037de1f99498c16eea71ecd414af8f56
--- /dev/null
+++ b/res/xml/provider_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/org/smssecure/smssecure/ApplicationContext.java b/src/org/smssecure/smssecure/ApplicationContext.java
index f09736a3e364aebebec6ff5fe845f0df60f0bd76..efa0278a8d982c220e5570b50f913e0ff1441925 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.content.Intent;
import org.smssecure.smssecure.crypto.PRNGFixes;
import org.smssecure.smssecure.dependencies.InjectableType;
@@ -26,6 +27,8 @@ 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.XmppUtil;
+import org.smssecure.smssecure.service.XmppService;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.jobqueue.dependencies.DependencyInjector;
import org.whispersystems.jobqueue.requirements.NetworkRequirementProvider;
@@ -60,6 +63,7 @@ public class ApplicationContext extends Application implements DependencyInjecto
initializeRandomNumberFix();
initializeLogging();
initializeJobManager();
+ initializeXmppService();
}
@Override
@@ -94,6 +98,10 @@ public class ApplicationContext extends Application implements DependencyInjecto
.build();
}
+ private void initializeXmppService() {
+ XmppUtil.startService(this);
+ }
+
public void notifyMediaControlEvent() {
mediaNetworkRequirementProvider.notifyMediaControlEvent();
}
diff --git a/src/org/smssecure/smssecure/ApplicationPreferencesActivity.java b/src/org/smssecure/smssecure/ApplicationPreferencesActivity.java
index 364b69201ff3037bc3a137ab8bb8f317ddc49a92..29b5e396eec5b7c961e2fe4dc14df466e694b1dd 100644
--- a/src/org/smssecure/smssecure/ApplicationPreferencesActivity.java
+++ b/src/org/smssecure/smssecure/ApplicationPreferencesActivity.java
@@ -17,9 +17,11 @@
package org.smssecure.smssecure;
import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Build.VERSION;
import android.os.Bundle;
@@ -40,11 +42,12 @@ import org.smssecure.smssecure.preferences.AppearancePreferenceFragment;
import org.smssecure.smssecure.preferences.NotificationsPreferenceFragment;
import org.smssecure.smssecure.preferences.SmsMmsPreferenceFragment;
import org.smssecure.smssecure.preferences.ChatsPreferenceFragment;
+import org.smssecure.smssecure.preferences.XmppPreferenceFragment;
import org.smssecure.smssecure.service.KeyCachingService;
+import org.smssecure.smssecure.service.XmppService;
import org.smssecure.smssecure.util.Dialogs;
import org.smssecure.smssecure.util.DynamicLanguage;
import org.smssecure.smssecure.util.DynamicTheme;
-import org.smssecure.smssecure.util.task.ProgressDialogAsyncTask;
import org.smssecure.smssecure.util.ResUtil;
import org.smssecure.smssecure.util.SilencePreferences;
import org.whispersystems.libaxolotl.util.guava.Optional;
@@ -68,6 +71,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
+ private static final String PREFERENCE_CATEGORY_XMPP = "preference_category_xmpp";
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
private static final String PREFERENCE_ABOUT = "preference_about";
@@ -136,10 +140,21 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
}
public static class ApplicationPreferenceFragment extends PreferenceFragment {
+ private BroadcastReceiver xmppUpdateReceiver;
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
+ xmppUpdateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ setCategorySummaries();
+ }
+ };
+
+ getActivity().registerReceiver(xmppUpdateReceiver, new IntentFilter(XmppService.XMPP_CONNECTIVITY_EVENT));
+
addPreferencesFromResource(R.xml.preferences);
MasterSecret masterSecret = getArguments().getParcelable("master_secret");
@@ -153,6 +168,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_APPEARANCE));
this.findPreference(PREFERENCE_CATEGORY_CHATS)
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_CHATS));
+ this.findPreference(PREFERENCE_CATEGORY_XMPP)
+ .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_XMPP));
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
.setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_ADVANCED));
}
@@ -175,6 +192,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.setSummary(AppearancePreferenceFragment.getSummary(getActivity()));
this.findPreference(PREFERENCE_CATEGORY_CHATS)
.setSummary(ChatsPreferenceFragment.getSummary(getActivity()));
+ this.findPreference(PREFERENCE_CATEGORY_XMPP)
+ .setSummary(XmppPreferenceFragment.getSummary(getActivity()));
String version = String.format(this.getString(R.string.preferences__about_version), BuildConfig.VERSION_NAME);
String buildID = String.format(this.getString(R.string.preferences__about_build_id), BuildConfig.BUILD_GIT_COMMIT);
@@ -214,6 +233,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
case PREFERENCE_CATEGORY_CHATS:
fragment = new ChatsPreferenceFragment();
break;
+ case PREFERENCE_CATEGORY_XMPP:
+ fragment = new XmppPreferenceFragment();
+ break;
case PREFERENCE_CATEGORY_ADVANCED:
fragment = new AdvancedPreferenceFragment();
break;
@@ -234,5 +256,12 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
return true;
}
}
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (xmppUpdateReceiver != null) getActivity().unregisterReceiver(xmppUpdateReceiver);
+ }
+
}
}
diff --git a/src/org/smssecure/smssecure/BasicIntroFragment.java b/src/org/smssecure/smssecure/BasicIntroFragment.java
index 35e9e640b2fa250a06e871a62d1ce84d7043caba..8863827f714faef4bc0a4f571d8d40a8a133ac69 100644
--- a/src/org/smssecure/smssecure/BasicIntroFragment.java
+++ b/src/org/smssecure/smssecure/BasicIntroFragment.java
@@ -1,7 +1,14 @@
package org.smssecure.smssecure;
+import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
+import android.text.method.LinkMovementMethod;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.text.TextPaint;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -10,20 +17,30 @@ import android.widget.TextView;
public class BasicIntroFragment extends Fragment {
- private static final String ARG_DRAWABLE = "drawable";
- private static final String ARG_TEXT = "text";
- private static final String ARG_SUBTEXT = "subtext";
+ private static final String TAG = BasicIntroFragment.class.getSimpleName();
- private int drawable;
- private int text;
- private int subtext;
+ private static final String ARG_DRAWABLE = "drawable";
+ private static final String ARG_TEXT = "text";
+ private static final String ARG_SUBTEXT = "subtext";
+ private static final String ARG_LINK_TEXT = "link_text";
+ private static final String ARG_ACTIVITY = "activity";
- public static BasicIntroFragment newInstance(int drawable, int text, int subtext) {
+ private int drawable;
+ private int text;
+ private int subtext;
+ private int linkText;
+ private String activity;
+
+ public static BasicIntroFragment newInstance(int drawable, int text, int subtext, int linkText, String activity) {
BasicIntroFragment fragment = new BasicIntroFragment();
Bundle args = new Bundle();
args.putInt(ARG_DRAWABLE, drawable);
args.putInt(ARG_TEXT, text);
args.putInt(ARG_SUBTEXT, subtext);
+ args.putInt(ARG_LINK_TEXT, linkText);
+
+ args.putString(ARG_ACTIVITY, activity);
+
fragment.setArguments(args);
return fragment;
}
@@ -34,9 +51,12 @@ public class BasicIntroFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
- drawable = getArguments().getInt(ARG_DRAWABLE);
- text = getArguments().getInt(ARG_TEXT );
- subtext = getArguments().getInt(ARG_SUBTEXT );
+ drawable = getArguments().getInt(ARG_DRAWABLE );
+ text = getArguments().getInt(ARG_TEXT );
+ subtext = getArguments().getInt(ARG_SUBTEXT );
+ linkText = getArguments().getInt(ARG_LINK_TEXT);
+
+ activity = getArguments().getString(ARG_ACTIVITY);
}
}
@@ -47,7 +67,35 @@ public class BasicIntroFragment extends Fragment {
((ImageView)v.findViewById(R.id.watermark)).setImageResource(drawable);
((TextView)v.findViewById(R.id.blurb)).setText(text);
- ((TextView)v.findViewById(R.id.subblurb)).setText(subtext);
+
+ if (linkText != -1) {
+ TextView subBlurbView = (TextView) v.findViewById(R.id.subblurb);
+ SpannableString spannableString = new SpannableString(getString(subtext) + " " + getString(linkText));
+ ClickableSpan clickableSpan = new ClickableSpan() {
+ @Override
+ public void onClick(View widget) {
+ if (activity != null) {
+ try {
+ Intent intent = new Intent(getActivity(), Class.forName(activity));
+ getActivity().startActivity(intent);
+ } catch (ClassNotFoundException e) {
+ Log.w(TAG, e);
+ }
+ }
+ }
+
+ @Override
+ public void updateDrawState(TextPaint textPaint) {
+ textPaint.linkColor = getResources().getColor(org.smssecure.smssecure.R.color.white);
+ super.updateDrawState(textPaint);
+ }
+ };
+ spannableString.setSpan(clickableSpan, getString(subtext).length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ subBlurbView.setMovementMethod(LinkMovementMethod.getInstance());
+ subBlurbView.setText(spannableString);
+ } else {
+ ((TextView)v.findViewById(R.id.subblurb)).setText(subtext);
+ }
return v;
}
diff --git a/src/org/smssecure/smssecure/ConversationActivity.java b/src/org/smssecure/smssecure/ConversationActivity.java
index 01bac3a26cb2e48e98ed7831f4f73b396c16f617..f1487491ad2f3d2473f2849cc262d1a94d910773 100644
--- a/src/org/smssecure/smssecure/ConversationActivity.java
+++ b/src/org/smssecure/smssecure/ConversationActivity.java
@@ -102,10 +102,12 @@ import org.smssecure.smssecure.recipients.RecipientFormattingException;
import org.smssecure.smssecure.recipients.Recipients;
import org.smssecure.smssecure.recipients.Recipients.RecipientsModifiedListener;
import org.smssecure.smssecure.service.KeyCachingService;
+import org.smssecure.smssecure.service.XmppService;
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.sms.OutgoingXmppExchangeMessage;
import org.smssecure.smssecure.util.concurrent.AssertedSuccessListener;
import org.smssecure.smssecure.util.CharacterCalculator.CharacterState;
import org.smssecure.smssecure.util.Dialogs;
@@ -120,9 +122,12 @@ 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.SubscriptionManagerCompat;
+import org.smssecure.smssecure.util.XmppUtil;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.util.guava.Optional;
+import org.jivesoftware.smack.packet.Presence;
+
import java.io.IOException;
import java.util.List;
@@ -174,6 +179,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private AttachmentTypeSelectorAdapter attachmentAdapter;
private AttachmentManager attachmentManager;
private BroadcastReceiver securityUpdateReceiver;
+ private BroadcastReceiver xmppUpdateReceiver;
private BroadcastReceiver groupUpdateReceiver;
private EmojiDrawer emojiDrawer;
private EmojiToggle emojiToggle;
@@ -280,6 +286,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
saveDraft();
if (recipients != null) recipients.removeListener(this);
if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver);
+ if (xmppUpdateReceiver != null) unregisterReceiver(xmppUpdateReceiver);
if (groupUpdateReceiver != null) unregisterReceiver(groupUpdateReceiver);
super.onDestroy();
}
@@ -333,6 +340,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (isSingleConversation() && isEncryptedConversation) {
inflater.inflate(R.menu.conversation_secure_identity, menu);
+ if (SilencePreferences.isXmppRegistered(this)) inflater.inflate(R.menu.conversation_secure_xmpp, menu.findItem(R.id.menu_security).getSubMenu());
inflater.inflate(R.menu.conversation_secure_sms, menu.findItem(R.id.menu_security).getSubMenu());
} else if (isSingleConversation()) {
inflater.inflate(R.menu.conversation_insecure_no_push, menu);
@@ -374,23 +382,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_display_xmpp_address: handleDisplayXmppAddress(); return true;
+ case R.id.menu_start_xmpp_exchange: handleStartXmppExchange(); return true;
+ case R.id.menu_enter_xmpp_address_manually: handleEnterXmppAddressManually(); 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;
}
return false;
@@ -531,6 +542,59 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
builder.show();
}
+ private void handleDisplayXmppAddress() {
+ new XmppAddressDialog(this).show();
+ }
+
+ private void handleStartXmppExchange() {
+ if (getRecipients() == null) {
+ Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient),
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ if (TelephonyUtil.isMyPhoneNumber(this, recipients.getPrimaryRecipient().getNumber())) {
+ Toast.makeText(this, getString(R.string.ConversationActivity_recipient_self),
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ final Recipients recipients = getRecipients();
+ final Recipient recipient = recipients.getPrimaryRecipient();
+ final int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
+
+ OutgoingXmppExchangeMessage xmppExchangeMessage =
+ new OutgoingXmppExchangeMessage(new OutgoingTextMessage(getRecipients(),
+ SilencePreferences.getXmppUsername(this) + "@" + SilencePreferences.getXmppHostname(this),
+ subscriptionId));
+
+ final Context context = getApplicationContext();
+
+ new AsyncTask() {
+ @Override
+ protected Long doInBackground(OutgoingXmppExchangeMessage... messages) {
+ return MessageSender.send(context, masterSecret, messages[0], threadId, false);
+ }
+
+ @Override
+ protected void onPostExecute(Long threadId) {
+ long allocatedThreadId;
+ if (threadId == -1) {
+ allocatedThreadId = DatabaseFactory.getThreadDatabase(getApplicationContext()).getThreadIdFor(recipients);
+ } else {
+ allocatedThreadId = threadId;
+ }
+ Log.w(TAG, "Refreshing thread "+allocatedThreadId+"...");
+ sendComplete(allocatedThreadId);
+ }
+ }.execute(xmppExchangeMessage);
+ }
+
+ private void handleEnterXmppAddressManually() {
+ Recipient recipient = recipients.getPrimaryRecipient();
+ if (recipient != null) new XmppEnterAddressDialog(this, recipient).show();
+ }
+
private void handleAbortSecureSession() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.ConversationActivity_abort_secure_session_confirmation);
@@ -750,11 +814,29 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
sendButton.resetAvailableTransports(isMediaMessage);
- if (!isSecureSmsDestination ) sendButton.disableTransport(Type.SECURE_SMS);
+ if (!isSecureSmsDestination){
+ sendButton.disableTransport(Type.SECURE_SMS);
+ sendButton.disableTransport(Type.SECURE_XMPP);
+ sendButton.disableTransport(Type.SECURE_XMPP_OFFLINE);
+ }
if (recipients.isGroupRecipient()) sendButton.disableTransport(Type.INSECURE_SMS);
if (isSecureSmsDestination) {
- sendButton.setDefaultTransport(Type.SECURE_SMS);
+ if (primaryRecipient.getXmppJid() != null && XmppUtil.isXmppAvailable(this)) {
+ Presence presence = XmppService.getInstance().getRecipientPresence(primaryRecipient.getXmppJid());
+ if (presence == null ||
+ !presence.isAvailable()) {
+ sendButton.setDefaultTransport(SilencePreferences.isXmppForced(this) ? Type.SECURE_XMPP_OFFLINE : Type.SECURE_SMS);
+ sendButton.disableTransport(Type.SECURE_XMPP);
+ } else {
+ sendButton.setDefaultTransport(Type.SECURE_XMPP);
+ sendButton.disableTransport(Type.SECURE_XMPP_OFFLINE);
+ }
+ } else {
+ sendButton.setDefaultTransport(Type.SECURE_SMS);
+ sendButton.disableTransport(Type.SECURE_XMPP);
+ sendButton.disableTransport(Type.SECURE_XMPP_OFFLINE);
+ }
} else {
sendButton.setDefaultTransport(Type.INSECURE_SMS);
}
@@ -841,6 +923,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
sendButton.addOnTransportChangedListener(new OnTransportChangedListener() {
@Override
public void onChange(TransportOption newTransport, boolean manuallySelected) {
+ if (newTransport.getType() == Type.SECURE_XMPP_OFFLINE && !SilencePreferences.isXmppForced(ConversationActivity.this)) {
+ Toast.makeText(ConversationActivity.this,
+ R.string.ConversationActivity_recipient_is_offline_xmpp_messages_may_be_delayed,
+ Toast.LENGTH_LONG).show();
+ }
calculateCharactersRemaining();
composeText.setTransport(newTransport);
buttonToggle.getBackground().setColorFilter(newTransport.getBackgroundColor(), Mode.MULTIPLY);
@@ -906,6 +993,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
setBlockedUserState(recipients);
setActionBarColor(recipients.getColor());
updateRecipientPreferences();
+ initializeSecurity();
}
});
}
@@ -917,13 +1005,22 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
long eventThreadId = intent.getLongExtra("thread_id", -1);
if (eventThreadId == threadId || eventThreadId == -2) {
- initializeSecurity();
updateRecipientPreferences();
+ initializeSecurity();
calculateCharactersRemaining();
}
}
};
+ xmppUpdateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ initializeSecurity();
+ updateRecipientPreferences();
+ calculateCharactersRemaining();
+ }
+ };
+
groupUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -942,6 +1039,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new IntentFilter(SecurityEvent.SECURITY_UPDATE_EVENT),
KeyCachingService.KEY_PERMISSION, null);
+ registerReceiver(xmppUpdateReceiver,
+ new IntentFilter(XmppService.XMPP_CONNECTIVITY_EVENT));
+
registerReceiver(groupUpdateReceiver,
new IntentFilter(GroupDatabase.DATABASE_UPDATE_ACTION));
}
@@ -1230,7 +1330,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new AsyncTask() {
@Override
protected Long doInBackground(OutgoingMediaMessage... messages) {
- return MessageSender.send(context, masterSecret, messages[0], threadId, true);
+ boolean isXmpp = isXmppType(sendButton.getSelectedTransport().getType());
+ return MessageSender.send(context, masterSecret, messages[0], threadId, isXmpp);
}
@Override
@@ -1257,7 +1358,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new AsyncTask() {
@Override
protected Long doInBackground(OutgoingTextMessage... messages) {
- return MessageSender.send(context, masterSecret, messages[0], threadId, true);
+ boolean isXmpp = isXmppType(sendButton.getSelectedTransport().getType());
+ return MessageSender.send(context, masterSecret, messages[0], threadId, isXmpp);
}
@Override
@@ -1267,6 +1369,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(message);
}
+ private boolean isXmppType(Type type) {
+ return type == Type.SECURE_XMPP || type == Type.SECURE_XMPP_OFFLINE;
+ }
+
private void updateToggleButtonState() {
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
buttonToggle.display(attachButton);
diff --git a/src/org/smssecure/smssecure/ConversationItem.java b/src/org/smssecure/smssecure/ConversationItem.java
index 4fb367909f78084ed5a22821519e263943b29d0a..faa20cf9adb528ae3e9692e7e88c726bfc9b32bd 100644
--- a/src/org/smssecure/smssecure/ConversationItem.java
+++ b/src/org/smssecure/smssecure/ConversationItem.java
@@ -99,6 +99,7 @@ public class ConversationItem extends LinearLayout
private TextView indicatorText;
private TextView groupStatusText;
private ImageView secureImage;
+ private ImageView xmppImage;
private AvatarImageView contactPhoto;
private DeliveryStatusView deliveryStatusIndicator;
private AlertView alertView;
@@ -142,6 +143,7 @@ public class ConversationItem extends LinearLayout
this.indicatorText = (TextView) findViewById(R.id.indicator_text);
this.groupStatusText = (TextView) findViewById(R.id.group_message_status);
this.secureImage = (ImageView) findViewById(R.id.secure_indicator);
+ this.xmppImage = (ImageView) findViewById(R.id.xmpp_indicator);
this.deliveryStatusIndicator = (DeliveryStatusView) findViewById(R.id.delivery_status);
this.alertView = (AlertView) findViewById(R.id.indicators_parent);
this.mmsDownloadButton = (Button) findViewById(R.id.mms_download_button);
@@ -311,6 +313,7 @@ public class ConversationItem extends LinearLayout
indicatorText.setVisibility(View.GONE);
secureImage.setVisibility(messageRecord.isSecure() ? View.VISIBLE : View.GONE);
+ xmppImage.setVisibility(messageRecord.isXmpp() ? View.VISIBLE : View.GONE);
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
dateText.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
@@ -603,74 +606,8 @@ public class ConversationItem extends LinearLayout
!messageRecord.isStaleKeyExchange())
{
handleKeyExchangeClicked();
- } else if (messageRecord.isPendingSmsFallback()) {
- handleMessageApproval();
}
}
}
- private void handleMessageApproval() {
- final int title;
- final int message;
-
- if (messageRecord.isPendingSecureSmsFallback()) {
- //TODO: Remove push code
- title = -1;
-
- message = -1;
- } else {
- if (messageRecord.isMms()) title = R.string.ConversationItem_click_to_approve_unencrypted_mms_dialog_title;
- else title = R.string.ConversationItem_click_to_approve_unencrypted_sms_dialog_title;
-
- message = R.string.ConversationItem_click_to_approve_unencrypted_dialog_message;
- }
-
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(title);
-
- if (message > -1) builder.setMessage(message);
-
- builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- if (messageRecord.isMms()) {
- MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
- if (messageRecord.isPendingInsecureSmsFallback()) {
- database.markAsInsecure(messageRecord.getId());
- }
- database.markAsOutbox(messageRecord.getId());
- database.markAsForcedSms(messageRecord.getId());
-
- ApplicationContext.getInstance(context)
- .getJobManager()
- .add(new MmsSendJob(context, messageRecord.getId()));
- } else {
- SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
- if (messageRecord.isPendingInsecureSmsFallback()) {
- database.markAsInsecure(messageRecord.getId());
- }
- database.markAsOutbox(messageRecord.getId());
- database.markAsForcedSms(messageRecord.getId());
-
- ApplicationContext.getInstance(context)
- .getJobManager()
- .add(new SmsSendJob(context, messageRecord.getId(),
- messageRecord.getIndividualRecipient().getNumber()));
- }
- }
- });
-
- builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- if (messageRecord.isMms()) {
- DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageRecord.getId());
- } else {
- DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageRecord.getId());
- }
- }
- });
- builder.show();
- }
-
}
diff --git a/src/org/smssecure/smssecure/ConversationListFragment.java b/src/org/smssecure/smssecure/ConversationListFragment.java
index 45e7e2fa0651214d449efb13c0e76c8d83fab47c..9c2d70ae9959129b1fc76082feea1953f78dfa0e 100644
--- a/src/org/smssecure/smssecure/ConversationListFragment.java
+++ b/src/org/smssecure/smssecure/ConversationListFragment.java
@@ -17,9 +17,11 @@
package org.smssecure.smssecure;
import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
@@ -63,6 +65,7 @@ import org.smssecure.smssecure.components.reminder.Reminder;
import org.smssecure.smssecure.components.reminder.ReminderView;
import org.smssecure.smssecure.components.reminder.StoreRatingReminder;
import org.smssecure.smssecure.components.reminder.SystemSmsImportReminder;
+import org.smssecure.smssecure.components.reminder.XmppConnectivityDisconnectedReminder;
import org.smssecure.smssecure.crypto.MasterCipher;
import org.smssecure.smssecure.crypto.MasterSecret;
import org.smssecure.smssecure.crypto.SessionUtil;
@@ -75,6 +78,7 @@ import org.smssecure.smssecure.notifications.MessageNotifier;
import org.smssecure.smssecure.mms.OutgoingMediaMessage;
import org.smssecure.smssecure.mms.OutgoingSecureMediaMessage;
import org.smssecure.smssecure.recipients.Recipients;
+import org.smssecure.smssecure.service.XmppService;
import org.smssecure.smssecure.sms.MessageSender;
import org.smssecure.smssecure.sms.OutgoingEncryptedMessage;
import org.smssecure.smssecure.sms.OutgoingTextMessage;
@@ -106,6 +110,8 @@ public class ConversationListFragment extends Fragment
private String queryFilter = "";
private boolean archive;
+ private BroadcastReceiver xmppUpdateReceiver;
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
@@ -151,6 +157,7 @@ public class ConversationListFragment extends Fragment
super.onResume();
initializeReminders();
+ initializeReceiver();
list.getAdapter().notifyDataSetChanged();
}
@@ -181,7 +188,9 @@ public class ConversationListFragment extends Fragment
} else if (DeliveryReportsReminder.isEligible(context)) {
return Optional.of((new DeliveryReportsReminder(context)));
} else if (StoreRatingReminder.isEligible(context)) {
- return Optional.of((new StoreRatingReminder(context)));
+ return Optional.of(new StoreRatingReminder(context));
+ } else if (XmppConnectivityDisconnectedReminder.isEligible(context)) {
+ return Optional.of(new XmppConnectivityDisconnectedReminder(context));
} else {
return Optional.absent();
}
@@ -195,6 +204,17 @@ public class ConversationListFragment extends Fragment
}.execute(getActivity());
}
+ private void initializeReceiver() {
+ xmppUpdateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ initializeReminders();
+ }
+ };
+
+ getActivity().registerReceiver(xmppUpdateReceiver, new IntentFilter(XmppService.XMPP_CONNECTIVITY_EVENT));
+ }
+
private void initializeListAdapter() {
list.setAdapter(new ConversationListAdapter(getActivity(), masterSecret, locale, null, this));
getLoaderManager().restartLoader(0, null, this);
@@ -527,6 +547,12 @@ public class ConversationListFragment extends Fragment
actionMode = null;
}
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (xmppUpdateReceiver != null) getActivity().unregisterReceiver(xmppUpdateReceiver);
+ }
+
private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback {
public ArchiveListenerCallback() {
diff --git a/src/org/smssecure/smssecure/IntroScreenActivity.java b/src/org/smssecure/smssecure/IntroScreenActivity.java
index c5dd8bb2f56e94195e9fc9ddf9fcb2616365ce1f..6432a82493ec9b85439db5497a1fa69c959ecf10 100644
--- a/src/org/smssecure/smssecure/IntroScreenActivity.java
+++ b/src/org/smssecure/smssecure/IntroScreenActivity.java
@@ -45,15 +45,27 @@ public class IntroScreenActivity extends BaseActionBarActivity {
add(new IntroPage(0xFF7568AE,
BasicIntroFragment.newInstance(R.drawable.splash_logo,
R.string.IntroScreenActivity_welcome_to_silence,
- R.string.IntroScreenActivity_silence_description)));
+ R.string.IntroScreenActivity_silence_description,
+ -1,
+ null)));
add(new IntroPage(0xFF7568AE,
BasicIntroFragment.newInstance(R.drawable.splash_padlock,
R.string.IntroScreenActivity_encrypt_your_messages,
- R.string.IntroScreenActivity_encrypt_your_messages_description)));
+ R.string.IntroScreenActivity_encrypt_your_messages_description,
+ -1,
+ null)));
add(new IntroPage(0xFF7568AE,
- BasicIntroFragment.newInstance(R.drawable.splash_open_padlock,
- R.string.IntroScreenActivity_talk_to_everyone,
- R.string.IntroScreenActivity_talk_to_everyone_description)));
+ BasicIntroFragment.newInstance(R.drawable.splash_open_padlock,
+ R.string.IntroScreenActivity_talk_to_everyone,
+ R.string.IntroScreenActivity_talk_to_everyone_description,
+ -1,
+ null)));
+ add(new IntroPage(0xFF7568AE,
+ BasicIntroFragment.newInstance(R.drawable.splash_xmpp,
+ R.string.IntroScreenActivity_protect_your_metadata,
+ R.string.IntroScreenActivity_protect_your_metadata_description,
+ R.string.IntroScreenActivity_protect_your_metadata_link,
+ "org.smssecure.smssecure.XmppRegisterActivity")));
}
});
@@ -98,7 +110,7 @@ public class IntroScreenActivity extends BaseActionBarActivity {
if (numberOfPages > 1) {
try {
// For some reason this seems to throw an NPE on Android 2.3 - work around it for now
- // See https://github.com/Silence/Silence/issues/311
+ // See https://github.com/SilenceIM/Silence/issues/311
indicator.setViewPager(pager);
indicator.setOnPageChangeListener(new OnPageChangeListener(introScreen.get()));
}
diff --git a/src/org/smssecure/smssecure/MessageDetailsActivity.java b/src/org/smssecure/smssecure/MessageDetailsActivity.java
index 8c74d8d8d50a29e34258483ae637a407fe2a4567..6230ee65127076833ca31e3c2f23fd6679f021e0 100644
--- a/src/org/smssecure/smssecure/MessageDetailsActivity.java
+++ b/src/org/smssecure/smssecure/MessageDetailsActivity.java
@@ -173,6 +173,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
transportText = "-";
} else if (messageRecord.isPending()) {
transportText = getString(R.string.ConversationFragment_pending);
+ } else if (messageRecord.isXmpp()) {
+ transportText = getString(R.string.ConversationFragment_xmpp);
} else if (messageRecord.isMms()) {
transportText = getString(R.string.ConversationFragment_mms);
} else {
@@ -216,7 +218,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
private void updateRecipients(MessageRecord messageRecord, Recipients recipients) {
final int toFromRes;
- if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
+ if (messageRecord.isMms() && !messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__with;
} else if (messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__to;
diff --git a/src/org/smssecure/smssecure/ShareActivity.java b/src/org/smssecure/smssecure/ShareActivity.java
index 8d983952367263afa42fb246db650aa68c212074..d8e6c08986df8bcd2ecd9b1234a2d863ae9498fb 100644
--- a/src/org/smssecure/smssecure/ShareActivity.java
+++ b/src/org/smssecure/smssecure/ShareActivity.java
@@ -30,6 +30,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Toast;
import org.smssecure.smssecure.crypto.MasterSecret;
import org.smssecure.smssecure.mms.PartAuthority;
@@ -62,6 +63,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
private Uri resolvedExtra;
private String mimeType;
private boolean isPassingAlongMedia;
+ private String xmppAddress;
@Override
protected void onPreCreate() {
@@ -79,6 +81,8 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
initFragment(R.id.drawer_layout, new ShareFragment(), masterSecret);
initializeMedia();
+ Uri xmppUri = getIntent().getData();
+ if (xmppUri != null) xmppAddress = xmppUri.getEncodedFragment();
}
@Override
@@ -93,7 +97,11 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
- getSupportActionBar().setTitle(R.string.ShareActivity_share_with);
+ if (xmppAddress != null && !xmppAddress.equals("")) {
+ getSupportActionBar().setTitle(R.string.ShareActivity_add_xmpp_address);
+ } else {
+ getSupportActionBar().setTitle(R.string.ShareActivity_share_with);
+ }
}
@Override
@@ -157,6 +165,11 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
}
private void createConversation(long threadId, Recipients recipients, int distributionType) {
+ if (xmppAddress != null && !xmppAddress.equals("")) {
+ recipients.getPrimaryRecipient().setXmppJid(xmppAddress);
+ Toast.makeText(getApplicationContext(), R.string.ShareActivity_xmpp_address_added, Toast.LENGTH_LONG).show();
+ }
+
final Intent intent = getBaseShareIntent(ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
@@ -216,4 +229,4 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
ViewUtil.fadeOut(progressWheel, 300);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/org/smssecure/smssecure/TransportOption.java b/src/org/smssecure/smssecure/TransportOption.java
index 594b8177a64fa19762c95baba959104a1808a14e..7a3d59b1b4bc089682bc16f265d23d1af824547f 100644
--- a/src/org/smssecure/smssecure/TransportOption.java
+++ b/src/org/smssecure/smssecure/TransportOption.java
@@ -13,7 +13,9 @@ public class TransportOption {
public enum Type {
INSECURE_SMS,
- SECURE_SMS
+ SECURE_SMS,
+ SECURE_XMPP,
+ SECURE_XMPP_OFFLINE
}
private final int drawable;
diff --git a/src/org/smssecure/smssecure/TransportOptions.java b/src/org/smssecure/smssecure/TransportOptions.java
index 5de94f57bc8949c912e7332d956c92c3bf6400ae..46b9f7897390d0d2f2ca11ff11b8770e6a781d5a 100644
--- a/src/org/smssecure/smssecure/TransportOptions.java
+++ b/src/org/smssecure/smssecure/TransportOptions.java
@@ -11,6 +11,8 @@ import org.smssecure.smssecure.util.SmsCharacterCalculator;
import org.smssecure.smssecure.util.EncryptedSmsCharacterCalculator;
import org.smssecure.smssecure.util.dualsim.SubscriptionInfoCompat;
import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat;
+import org.smssecure.smssecure.util.XmppUtil;
+import org.smssecure.smssecure.util.XmppCharacterCalculator;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.LinkedList;
@@ -146,6 +148,19 @@ public class TransportOptions {
new EncryptedSmsCharacterCalculator()));
}
+ if (XmppUtil.isXmppAvailable(context)){
+ results.addAll(getTransportOptionsForSimCards(Type.SECURE_XMPP, R.drawable.ic_send_secure_white_24dp,
+ context.getResources().getColor(R.color.silence_xmpp),
+ context.getString(R.string.ConversationActivity_transport_secure_xmpp),
+ context.getString(R.string.conversation_activity__type_message_xmpp_secure),
+ new XmppCharacterCalculator()));
+ results.addAll(getTransportOptionsForSimCards(Type.SECURE_XMPP_OFFLINE, R.drawable.ic_send_secure_white_24dp,
+ context.getResources().getColor(R.color.silence_xmpp_offline),
+ context.getString(R.string.ConversationActivity_transport_secure_xmpp),
+ context.getString(R.string.conversation_activity__type_message_xmpp_secure),
+ new XmppCharacterCalculator()));
+ }
+
return results;
}
diff --git a/src/org/smssecure/smssecure/XmppAddressDialog.java b/src/org/smssecure/smssecure/XmppAddressDialog.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a689c0156383ff6e6ab16e34bebb894ccb24a43
--- /dev/null
+++ b/src/org/smssecure/smssecure/XmppAddressDialog.java
@@ -0,0 +1,86 @@
+package org.smssecure.smssecure;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.smssecure.smssecure.util.SilencePreferences;
+import org.smssecure.smssecure.util.ServiceUtil;
+
+import java.io.IOException;
+
+/**
+ * Activity for displaying XMPP address.
+ */
+
+public class XmppAddressDialog extends AlertDialog {
+ private static final String TAG = XmppAddressDialog.class.getSimpleName();
+
+ public static final String TEXT_PLAIN = "text/plain";
+
+ private Context context;
+ private String silenceLink;
+
+ public XmppAddressDialog(@NonNull Context context) {
+ super(context);
+
+ this.context = context;
+
+ LayoutInflater layoutInflater = ((Activity)context).getLayoutInflater();
+ View view = layoutInflater.inflate(R.layout.xmpp_address_dialog, null);
+
+ setTitle(context.getString(R.string.XmppAddressDialog_share_xmpp_address));
+ silenceLink = "https://s.silence.im/#" + SilencePreferences.getXmppJid(context);
+
+ TextView xmppAddress = (TextView) view.findViewById(R.id.xmpp_address);
+ xmppAddress.setText(silenceLink);
+ xmppAddress.setOnClickListener(new CopyXmppAdressListener());
+
+ setView(view);
+ setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(android.R.string.ok), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {}
+ });
+ setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.XmppAddressDialog_share), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ shareXmppAddress();
+ }
+ });
+ }
+
+ private void shareXmppAddress() {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.XmppAddressDialog_here_is_my_silence_xmpp_address, silenceLink));
+ sendIntent.setType(TEXT_PLAIN);
+ context.startActivity(sendIntent);
+ }
+
+ @Override
+ public void show() {
+ super.show();
+ }
+
+ private class CopyXmppAdressListener implements View.OnClickListener {
+ @Override
+ public void onClick(View view) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ ServiceUtil.getClipboardManager(context).setText(silenceLink);
+ } else {
+ ClipData clipData = ClipData.newPlainText(context.getString(R.string.XmppAddressDialog_silence_xmpp_address), silenceLink);
+ ServiceUtil.getClipboardManager(context).setPrimaryClip(clipData);
+ }
+ Toast.makeText(context.getApplicationContext(),
+ context.getString(R.string.XmppAddressDialog_xmpp_address_copied),
+ Toast.LENGTH_LONG).show();
+ }
+ }
+}
diff --git a/src/org/smssecure/smssecure/XmppEnterAddressDialog.java b/src/org/smssecure/smssecure/XmppEnterAddressDialog.java
new file mode 100644
index 0000000000000000000000000000000000000000..ad7ab916f3247a8dc750abb618178abb2d59c604
--- /dev/null
+++ b/src/org/smssecure/smssecure/XmppEnterAddressDialog.java
@@ -0,0 +1,63 @@
+package org.smssecure.smssecure;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.smssecure.smssecure.recipients.Recipient;
+import org.smssecure.smssecure.util.SilencePreferences;
+import org.smssecure.smssecure.util.ServiceUtil;
+
+import java.io.IOException;
+
+/**
+ * Activity for entering XMPP address manually.
+ */
+
+public class XmppEnterAddressDialog extends AlertDialog {
+ private static final String TAG = XmppEnterAddressDialog.class.getSimpleName();
+
+ private Recipient recipient;
+ private View view;
+ private Context context;
+
+ public XmppEnterAddressDialog(@NonNull Context context, @NonNull Recipient recipient) {
+ super(context);
+ this.context = context;
+ this.recipient = recipient;
+
+ LayoutInflater layoutInflater = ((Activity)context).getLayoutInflater();
+
+ view = layoutInflater.inflate(R.layout.xmpp_enter_address_dialog, null);
+
+ TextView xmppText = (TextView) view.findViewById(R.id.xmpp_text);
+
+ xmppText.setText(context.getString(R.string.xmpp_enter_address_dialog__use_this_form_to_enter_the_xmpp_address_of, recipient.getName()));
+
+ setView(view);
+ setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), (OnClickListener) null);
+ setButton(AlertDialog.BUTTON_POSITIVE, context.getString(android.R.string.ok), new EnterXmppAdressListener());
+ }
+
+ @Override
+ public void show() {
+ super.show();
+ }
+
+ private class EnterXmppAdressListener implements AlertDialog.OnClickListener {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ TextView xmppAddress = (TextView) view.findViewById(R.id.xmpp_address);
+ if (xmppAddress != null) recipient.setXmppJid(xmppAddress.getText() == null ? null : xmppAddress.getText().toString());
+ Toast.makeText(context,
+ context.getString(R.string.XmppEnterAddressDialog_done),
+ Toast.LENGTH_LONG).show();
+ }
+ }
+}
diff --git a/src/org/smssecure/smssecure/XmppRegisterActivity.java b/src/org/smssecure/smssecure/XmppRegisterActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..c118f7a1ac09f0c16292ce8de92ec7269d296acc
--- /dev/null
+++ b/src/org/smssecure/smssecure/XmppRegisterActivity.java
@@ -0,0 +1,171 @@
+package org.smssecure.smssecure;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Looper;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.Toast;
+
+import org.smssecure.smssecure.util.DynamicLanguage;
+import org.smssecure.smssecure.util.DynamicTheme;
+import org.smssecure.smssecure.util.SilencePreferences;
+import org.smssecure.smssecure.util.task.ProgressDialogAsyncTask;
+import org.smssecure.smssecure.util.XmppUtil;
+import org.smssecure.smssecure.XmppRegisterCustomActivity;
+
+/**
+ * Activity for registering an XMPP account on one of the trusted servers in
+ * Silence or on a custom server.
+ */
+public class XmppRegisterActivity extends BaseActionBarActivity {
+ private static final String TAG = XmppRegisterActivity.class.getSimpleName();
+
+ private Context context;
+
+ private static final int REGISTERING_CUSTOM_ACTIVITY_RESULT_CODE = 667;
+
+ private DynamicTheme dynamicTheme = new DynamicTheme();
+ private DynamicLanguage dynamicLanguage = new DynamicLanguage();
+
+ private Button trustedServerButton;
+ private Button customServerButton;
+ private Button cancelButton;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ dynamicTheme.onCreate(this);
+ dynamicLanguage.onCreate(this);
+ super.onCreate(savedInstanceState);
+
+ this.context = XmppRegisterActivity.this;
+
+ if (SilencePreferences.isXmppRegistered(context)) {
+ Toast.makeText(context, R.string.XmppRegisterActivity__you_are_already_registered, Toast.LENGTH_LONG).show();
+ finishActivity();
+ }
+
+ setContentView(R.layout.register_xmpp_account);
+
+ initializeResources();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ dynamicTheme.onResume(this);
+ dynamicLanguage.onResume(this);
+ }
+
+ private void initializeResources() {
+ this.trustedServerButton = (Button) findViewById(R.id.trusted_server_button);
+ this.customServerButton = (Button) findViewById(R.id.custom_server_button);
+ this.cancelButton = (Button) findViewById(R.id.cancel_button);
+
+ this.trustedServerButton.setOnClickListener(new TrustedServerButtonClickListener());
+ this.customServerButton.setOnClickListener(new CustomServerButtonClickListener());
+ this.cancelButton.setOnClickListener(new CancelButtonClickListener());
+ }
+
+ private class CancelButtonClickListener implements OnClickListener {
+ public void onClick(View v) {
+ SilencePreferences.disableXmpp(context);
+ finishActivity();
+ }
+ }
+
+ private class TrustedServerButtonClickListener implements OnClickListener {
+ public void onClick(View v) {
+ new RegisterOnTrustedServerTask(context).execute();
+ }
+ }
+
+ private class CustomServerButtonClickListener implements OnClickListener {
+ public void onClick(View v) {
+ startActivityForResult(new Intent(context, XmppRegisterCustomActivity.class), REGISTERING_CUSTOM_ACTIVITY_RESULT_CODE);
+ }
+ }
+
+ private class RegisterOnTrustedServerTask extends ProgressDialogAsyncTask {
+ private final Context context;
+
+ public RegisterOnTrustedServerTask(final Context context) {
+ super(context,
+ context.getString(R.string.register_xmpp_account_registration),
+ context.getString(R.string.register_xmpp_account_creating_an_xmpp_account));
+ this.context = context;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ XmppUtil.tryToRegister(context);
+ new Thread() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ Toast.makeText(context.getApplicationContext(),
+ context.getString(R.string.XmppRegisterActivity__registered),
+ Toast.LENGTH_LONG).show();
+ Looper.loop();
+ }
+ }.start();
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ new Thread() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ Toast.makeText(context,
+ context.getString(R.string.XmppRegisterActivity__cannot_create_an_xmpp_account_please_try_again_later),
+ Toast.LENGTH_LONG).show();
+ Looper.loop();
+ }
+ }.start();
+ SilencePreferences.disableXmpp(context);
+ }
+ finishActivity();
+ return null;
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ SilencePreferences.disableXmpp(context);
+ finishActivity();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void finishActivity() {
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REGISTERING_CUSTOM_ACTIVITY_RESULT_CODE &&
+ resultCode == Activity.RESULT_OK)
+ {
+ new Thread() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ Toast.makeText(context.getApplicationContext(),
+ context.getString(R.string.XmppRegisterActivity__registered),
+ Toast.LENGTH_LONG).show();
+ Looper.loop();
+ }
+ }.start();
+ finishActivity();
+ }
+ }
+
+}
diff --git a/src/org/smssecure/smssecure/XmppRegisterCustomActivity.java b/src/org/smssecure/smssecure/XmppRegisterCustomActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..cf71a03e6b1952d986a046effcc1c881a232fbf4
--- /dev/null
+++ b/src/org/smssecure/smssecure/XmppRegisterCustomActivity.java
@@ -0,0 +1,145 @@
+package org.smssecure.smssecure;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import org.smssecure.smssecure.util.DynamicLanguage;
+import org.smssecure.smssecure.util.DynamicTheme;
+import org.smssecure.smssecure.util.SilencePreferences;
+import org.smssecure.smssecure.util.task.ProgressDialogAsyncTask;
+import org.smssecure.smssecure.util.XmppUtil;
+import org.smssecure.smssecure.util.XmppUtil.XmppServer;
+
+/**
+ * Activity for registering an XMPP account on a custom server.
+ *
+ * @author Bastien Le Querrec
+ */
+public class XmppRegisterCustomActivity extends BaseActionBarActivity {
+ private static final String TAG = XmppRegisterCustomActivity.class.getSimpleName();
+
+ public static final String REGISTERED_EXTRA = "REGISTERED";
+
+ private Context context;
+
+ private DynamicTheme dynamicTheme = new DynamicTheme();
+ private DynamicLanguage dynamicLanguage = new DynamicLanguage();
+
+ private EditText hostnameText;
+ private EditText portText;
+ private Integer port;
+
+ private Button connectButton;
+ private Button cancelButton;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ dynamicTheme.onCreate(this);
+ dynamicLanguage.onCreate(this);
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.register_custom_xmpp_server);
+
+ initializeResources();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ dynamicTheme.onResume(this);
+ dynamicLanguage.onResume(this);
+ }
+
+ private void initializeResources() {
+ this.connectButton = (Button) findViewById(R.id.connect_button);
+ this.cancelButton = (Button) findViewById(R.id.cancel_button);
+
+ this.hostnameText = (EditText) findViewById(R.id.hostname);
+ this.portText = (EditText) findViewById(R.id.port);
+
+ this.connectButton.setOnClickListener(new ConnectButtonClickListener());
+ this.cancelButton.setOnClickListener(new CancelButtonClickListener());
+
+ this.context = XmppRegisterCustomActivity.this;
+ }
+
+ private class CancelButtonClickListener implements OnClickListener {
+ public void onClick(View v) {
+ finishActivity(false);
+ }
+ }
+
+ private class ConnectButtonClickListener implements OnClickListener {
+ public void onClick(View v) {
+ try {
+ String portString = portText.getText().toString();
+ if (portString == null || portString.equals("")) {
+ port = null;
+ } else {
+ port = Integer.parseInt(portString);
+ if (port > 65535 || port < 1) throw new NumberFormatException();
+ }
+ new RegisterCustomServerTask(hostnameText.getText().toString(), port).execute();
+ } catch (NumberFormatException e) {
+ Log.w(TAG, e);
+ Snackbar.make(findViewById(android.R.id.content), getString(R.string.register_xmpp_account_invalid_port), Snackbar.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ private class RegisterCustomServerTask extends ProgressDialogAsyncTask {
+ private final XmppServer xmppServer;
+
+ public RegisterCustomServerTask(String hostname, Integer port) {
+ super(context,
+ context.getString(R.string.register_xmpp_account_registration),
+ context.getString(R.string.register_xmpp_account_creating_an_xmpp_account));
+
+ if (port == null) {
+ this.xmppServer = new XmppServer(hostname);
+ } else {
+ this.xmppServer = new XmppServer(hostname, port);
+ }
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ XmppUtil.register(context, xmppServer);
+ SilencePreferences.enableXmpp(context);
+ XmppUtil.startService(context);
+ finishActivity(true);
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ Snackbar.make(findViewById(android.R.id.content), getString(R.string.register_xmpp_account_cannot_register_on_this_server), Snackbar.LENGTH_LONG).show();
+ SilencePreferences.disableXmpp(context);
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ finishActivity(false);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void finishActivity(boolean registered) {
+ setResult(registered ? Activity.RESULT_OK : Activity.RESULT_CANCELED);
+ finish();
+ }
+
+}
diff --git a/src/org/smssecure/smssecure/components/reminder/Reminder.java b/src/org/smssecure/smssecure/components/reminder/Reminder.java
index 71ca5f029c18d2c1b8a40963c153464b0fb722ab..c37c5408727680b2ac0259f83bccb29826abc91e 100644
--- a/src/org/smssecure/smssecure/components/reminder/Reminder.java
+++ b/src/org/smssecure/smssecure/components/reminder/Reminder.java
@@ -2,6 +2,7 @@ package org.smssecure.smssecure.components.reminder;
import android.content.Context;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.view.View.OnClickListener;
@@ -13,9 +14,9 @@ public abstract class Reminder {
private OnClickListener okListener;
private OnClickListener dismissListener;
- public Reminder(@NonNull CharSequence title,
- @NonNull CharSequence text,
- @NonNull CharSequence buttonText)
+ public Reminder(@NonNull CharSequence title,
+ @NonNull CharSequence text,
+ @Nullable CharSequence buttonText)
{
this.title = title;
this.text = text;
diff --git a/src/org/smssecure/smssecure/components/reminder/ReminderView.java b/src/org/smssecure/smssecure/components/reminder/ReminderView.java
index 10a6ac48c9bc616ac61f195fb4286d38d6a3d58e..bcc81f9802510959872e673720cad4fa79e11215 100644
--- a/src/org/smssecure/smssecure/components/reminder/ReminderView.java
+++ b/src/org/smssecure/smssecure/components/reminder/ReminderView.java
@@ -51,7 +51,6 @@ public class ReminderView extends LinearLayout {
public void showReminder(final Reminder reminder) {
title.setText(reminder.getTitle());
text.setText(reminder.getText());
- acceptButton.setText(reminder.getButtonText());
acceptButton.setOnClickListener(new OnClickListener() {
@Override
@@ -60,6 +59,12 @@ public class ReminderView extends LinearLayout {
if (reminder.getOkListener() != null) reminder.getOkListener().onClick(v);
}
});
+ if (reminder.getButtonText() != null) {
+ acceptButton.setText(reminder.getButtonText());
+ acceptButton.setOnClickListener(reminder.getOkListener());
+ } else {
+ acceptButton.setVisibility(View.GONE);
+ }
if (reminder.isDismissable()) {
closeButton.setOnClickListener(new OnClickListener() {
diff --git a/src/org/smssecure/smssecure/components/reminder/XmppConnectivityDisconnectedReminder.java b/src/org/smssecure/smssecure/components/reminder/XmppConnectivityDisconnectedReminder.java
new file mode 100644
index 0000000000000000000000000000000000000000..926e548bba5fe08e8654ff8fa88ce877f98b8a88
--- /dev/null
+++ b/src/org/smssecure/smssecure/components/reminder/XmppConnectivityDisconnectedReminder.java
@@ -0,0 +1,29 @@
+package org.smssecure.smssecure.components.reminder;
+
+import android.content.Context;
+
+import org.smssecure.smssecure.R;
+import org.smssecure.smssecure.service.XmppService;
+import org.smssecure.smssecure.util.SilencePreferences;
+import org.smssecure.smssecure.util.XmppUtil;
+
+public class XmppConnectivityDisconnectedReminder extends Reminder {
+
+ public XmppConnectivityDisconnectedReminder(final Context context) {
+ super(context.getString(R.string.XmppService_cannot_reach_xmpp_server),
+ context.getString(R.string.XmppService_xmpp_features_in_silence_are_disabled),
+ null);
+ }
+
+ public static boolean isEligible(Context context) {
+ XmppService xmppService = XmppService.getInstance();
+ return SilencePreferences.isXmppRegistered(context) &&
+ !XmppUtil.isXmppAvailable(context) &&
+ xmppService != null &&
+ !xmppService.getConnectionStatus().equals(XmppService.CONNECTED);
+ }
+
+ public boolean isDismissable() {
+ return false;
+ }
+}
diff --git a/src/org/smssecure/smssecure/crypto/SmsCipher.java b/src/org/smssecure/smssecure/crypto/SmsCipher.java
index c58a7d280bc385e47413689eb3dd075f525d6e16..c6b2c6fa686307d4c3f6d98be3ab0d39cfdf1d92 100644
--- a/src/org/smssecure/smssecure/crypto/SmsCipher.java
+++ b/src/org/smssecure/smssecure/crypto/SmsCipher.java
@@ -1,7 +1,10 @@
package org.smssecure.smssecure.crypto;
import android.content.Context;
+import android.os.AsyncTask;
+import android.util.Log;
+import org.smssecure.smssecure.database.DatabaseFactory;
import org.smssecure.smssecure.recipients.Recipient;
import org.smssecure.smssecure.recipients.RecipientFactory;
import org.smssecure.smssecure.recipients.RecipientFormattingException;
@@ -33,9 +36,12 @@ import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import java.io.IOException;
+import java.util.Map;
public class SmsCipher {
+ private static final String TAG = SmsCipher.class.getSimpleName();
+
private final SmsTransportDetails transportDetails = new SmsTransportDetails();
private final AxolotlStore axolotlStore;
@@ -53,13 +59,23 @@ public class SmsCipher {
WhisperMessage whisperMessage = new WhisperMessage(decoded);
SessionCipher sessionCipher = new SessionCipher(axolotlStore, new AxolotlAddress(message.getSender(), 1));
byte[] padded = sessionCipher.decrypt(whisperMessage);
- byte[] plaintext = transportDetails.getStrippedPaddingMessageBody(padded);
+ String plaintext = new String(transportDetails.getStrippedPaddingMessageBody(padded));
- if (message.isEndSession() && "TERMINATE".equals(new String(plaintext))) {
+ if (message.isEndSession() && "TERMINATE".equals(plaintext)) {
axolotlStore.deleteSession(new AxolotlAddress(message.getSender(), 1));
}
- return message.withMessageBody(new String(plaintext));
+ if (message.isXmppExchange() && message.getSender() != null) { // TODO: Jid validation
+ Log.w(TAG, "Proceeding XMPP exchange...");
+ Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSender(), true);
+
+ if (recipients.getPrimaryRecipient() != null) {
+ recipients.getPrimaryRecipient().setXmppJid(plaintext);
+ DatabaseFactory.getRecipientPreferenceDatabase(context).setXmppJid(recipients, plaintext);
+ }
+ }
+
+ return message.withMessageBody(plaintext);
} catch (IOException e) {
throw new InvalidMessageException(e);
}
diff --git a/src/org/smssecure/smssecure/database/CanonicalAddressDatabase.java b/src/org/smssecure/smssecure/database/CanonicalAddressDatabase.java
index 1075be701b1e4e1707546337b3adf87343e3d2a0..79fd4b8838f6aea6466f9fcc0a24ae73b788c611 100644
--- a/src/org/smssecure/smssecure/database/CanonicalAddressDatabase.java
+++ b/src/org/smssecure/smssecure/database/CanonicalAddressDatabase.java
@@ -146,30 +146,18 @@ public class CanonicalAddressDatabase {
}
public long getCanonicalAddressId(@NonNull String address) {
- try {
- long canonicalAddressId;
-
- if (isNumberAddress(address) && SilencePreferences.isPushRegistered(context)) {
- String localNumber = SilencePreferences.getLocalNumber(context);
-
- if (!ShortCodeUtil.isShortCode(localNumber, address)) {
- address = PhoneNumberFormatter.formatNumber(address, localNumber);
- }
- }
+ long canonicalAddressId;
- if ((canonicalAddressId = getCanonicalAddressFromCache(address)) != -1) {
- return canonicalAddressId;
- }
+ if ((canonicalAddressId = getCanonicalAddressFromCache(address)) != -1) {
+ return canonicalAddressId;
+ }
- canonicalAddressId = getCanonicalAddressIdFromDatabase(address);
+ canonicalAddressId = getCanonicalAddressIdFromDatabase(address);
- idCache.put(canonicalAddressId, address);
- addressCache.put(address, canonicalAddressId);
+ idCache.put(canonicalAddressId, address);
+ addressCache.put(address, canonicalAddressId);
- return canonicalAddressId;
- } catch (InvalidNumberException e) {
- throw new AssertionError(e);
- }
+ return canonicalAddressId;
}
public @NonNull List getCanonicalAddressIds(@NonNull List addresses) {
@@ -187,6 +175,28 @@ public class CanonicalAddressDatabase {
return cachedAddress == null ? -1L : cachedAddress;
}
+ public List getCanonicalNumbers() {
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ Cursor cursor = null;
+
+ try {
+ cursor = db.query(TABLE, null, null, null, null, null, null);
+
+ List recipients = new LinkedList<>();
+
+ while (cursor != null && cursor.moveToNext()) {
+ long canonicalId = cursor.getLong(cursor.getColumnIndexOrThrow(ID_COLUMN));
+ String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS_COLUMN));
+ if (canonicalId > 1) recipients.add(address);
+ }
+
+ return recipients;
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ }
+
private long getCanonicalAddressIdFromDatabase(@NonNull String address) {
Log.w(TAG, "Hitting DB on query [ADDRESS]");
diff --git a/src/org/smssecure/smssecure/database/DatabaseFactory.java b/src/org/smssecure/smssecure/database/DatabaseFactory.java
index 9040e011be5d152a5159a0a67f49ef4621f70600..4b5c306facddc0796f3c4c427c756955ee4afdbc 100644
--- a/src/org/smssecure/smssecure/database/DatabaseFactory.java
+++ b/src/org/smssecure/smssecure/database/DatabaseFactory.java
@@ -74,9 +74,9 @@ public class DatabaseFactory {
private static final int MIGRATED_CONVERSATION_LIST_STATUS_VERSION = 26;
private static final int INTRODUCED_SUBSCRIPTION_ID_VERSION = 28;
private static final int INTRODUCED_XMPP_TRANSPORT = 29;
- private static final int DATABASE_VERSION = 28;
+ public static final int DATABASE_VERSION = 29;
- private static final String DATABASE_NAME = "messages.db";
+ public static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
private static DatabaseFactory instance;
@@ -517,16 +517,6 @@ public class DatabaseFactory {
executeStatements(db, GroupDatabase.CREATE_INDEXS);
}
- @Override
- public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- db.beginTransaction();
-
- if (newVersion < INTRODUCED_XMPP_TRANSPORT) {}
-
- db.setTransactionSuccessful();
- db.endTransaction();
- }
-
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.beginTransaction();
@@ -828,6 +818,15 @@ public class DatabaseFactory {
db.execSQL("ALTER TABLE mms ADD COLUMN subscription_id INTEGER DEFAULT -1");
}
+ if (oldVersion < INTRODUCED_XMPP_TRANSPORT) {
+ try {
+ db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN xmpp_jid TEXT DEFAULT NULL");
+ db.execSQL("ALTER TABLE sms ADD COLUMN xmpp_id TEXT DEFAULT NULL");
+ } catch (Exception e) {
+ Log.w(TAG, e.getMessage());
+ }
+ }
+
db.setTransactionSuccessful();
db.endTransaction();
}
diff --git a/src/org/smssecure/smssecure/database/EncryptingSmsDatabase.java b/src/org/smssecure/smssecure/database/EncryptingSmsDatabase.java
index 48bed2e39f76c0505afb9634644d8038866fad98..8b4f702e4f0f2fbb9abc6e57bcbc87eda1a9ccb9 100644
--- a/src/org/smssecure/smssecure/database/EncryptingSmsDatabase.java
+++ b/src/org/smssecure/smssecure/database/EncryptingSmsDatabase.java
@@ -60,14 +60,13 @@ public class EncryptingSmsDatabase extends SmsDatabase {
}
public long insertMessageOutbox(MasterSecret masterSecret, long threadId,
- OutgoingTextMessage message, boolean forceSms,
- long timestamp)
+ OutgoingTextMessage message, long timestamp)
{
long type = Types.BASE_OUTBOX_TYPE;
message = message.withBody(getEncryptedBody(masterSecret, message.getMessageBody()));
type |= Types.ENCRYPTION_SYMMETRIC_BIT;
- return insertMessageOutbox(threadId, message, type, forceSms, timestamp);
+ return insertMessageOutbox(threadId, message, type, timestamp);
}
public Pair insertMessageInbox(MasterSecret masterSecret,
diff --git a/src/org/smssecure/smssecure/database/MmsDatabase.java b/src/org/smssecure/smssecure/database/MmsDatabase.java
index ed9af5e01e4836f742eb7b9f3f93d3c25741040e..8b5e7468b87baef4be54f6d2aa89df6dcc7f5423 100644
--- a/src/org/smssecure/smssecure/database/MmsDatabase.java
+++ b/src/org/smssecure/smssecure/database/MmsDatabase.java
@@ -50,6 +50,7 @@ import org.smssecure.smssecure.mms.IncomingMediaMessage;
import org.smssecure.smssecure.mms.OutgoingGroupMediaMessage;
import org.smssecure.smssecure.mms.OutgoingMediaMessage;
import org.smssecure.smssecure.mms.OutgoingSecureMediaMessage;
+import org.smssecure.smssecure.mms.OutgoingXmppMediaMessage;
import org.smssecure.smssecure.mms.SlideDeck;
import org.smssecure.smssecure.recipients.Recipient;
import org.smssecure.smssecure.recipients.RecipientFactory;
@@ -222,11 +223,7 @@ public class MmsDatabase extends MessagingDatabase {
group.add(retrieved.getAddresses().getFrom());
- if (SilencePreferences.isPushRegistered(context)) {
- localNumber = SilencePreferences.getLocalNumber(context);
- } else {
- localNumber = ServiceUtil.getTelephonyManager(context).getLine1Number();
- }
+ localNumber = ServiceUtil.getTelephonyManager(context).getLine1Number();
for (String cc : retrieved.getAddresses().getCc()) {
PhoneNumberUtil.MatchType match;
@@ -300,11 +297,6 @@ public class MmsDatabase extends MessagingDatabase {
notifyConversationListeners(getThreadIdForMessage(messageId));
}
- public void markAsForcedSms(long messageId) {
- updateMailboxBitmask(messageId, Types.PUSH_MESSAGE_BIT, Types.MESSAGE_FORCE_SMS_BIT);
- notifyConversationListeners(getThreadIdForMessage(messageId));
- }
-
public void markAsPendingSecureSmsFallback(long messageId) {
updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_PENDING_SECURE_SMS_FALLBACK);
notifyConversationListeners(getThreadIdForMessage(messageId));
@@ -361,8 +353,8 @@ public class MmsDatabase extends MessagingDatabase {
updateMailboxBitmask(messageId, Types.SECURE_MESSAGE_BIT, 0);
}
- public void markAsPush(long messageId) {
- updateMailboxBitmask(messageId, 0, Types.PUSH_MESSAGE_BIT);
+ public void markAsXmpp(long messageId) {
+ updateMailboxBitmask(messageId, 0, Types.XMPP_MESSAGE_BIT);
}
public void markAsDecryptFailed(long messageId, long threadId) {
@@ -455,6 +447,10 @@ public class MmsDatabase extends MessagingDatabase {
OutgoingMediaMessage message = new OutgoingMediaMessage(recipients, body, attachments, timestamp, subscriptionId,
!addresses.getBcc().isEmpty() ? ThreadDatabase.DistributionTypes.BROADCAST :
ThreadDatabase.DistributionTypes.DEFAULT);
+ if (Types.isXmppType(outboxType)) {
+ return new OutgoingXmppMediaMessage(message);
+ }
+
if (Types.isSecureType(outboxType)) {
return new OutgoingSecureMediaMessage(message);
}
@@ -566,8 +562,7 @@ public class MmsDatabase extends MessagingDatabase {
throws MmsException
{
return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId,
- Types.BASE_INBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT |
- (retrieved.isPushMessage() ? Types.PUSH_MESSAGE_BIT : 0));
+ Types.BASE_INBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT);
}
public Pair insertSecureMessageInbox(MasterSecret masterSecret,
@@ -587,8 +582,7 @@ public class MmsDatabase extends MessagingDatabase {
{
return insertMessageInbox(masterSecret, retrieved, "", threadId,
Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT |
- Types.ENCRYPTION_SYMMETRIC_BIT |
- (retrieved.isPushMessage() ? Types.PUSH_MESSAGE_BIT : 0));
+ Types.ENCRYPTION_SYMMETRIC_BIT);
}
public Pair insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId) {
@@ -647,13 +641,12 @@ public class MmsDatabase extends MessagingDatabase {
}
public long insertMessageOutbox(MasterSecret masterSecret, OutgoingMediaMessage message,
- long threadId, boolean forceSms)
+ long threadId)
throws MmsException
{
long type = Types.BASE_OUTBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT;
if (message.isSecure()) type |= Types.SECURE_MESSAGE_BIT;
- if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
if (message.isGroup()) {
if (((OutgoingGroupMediaMessage)message).isGroupUpdate()) type |= Types.GROUP_UPDATE_BIT;
diff --git a/src/org/smssecure/smssecure/database/MmsSmsColumns.java b/src/org/smssecure/smssecure/database/MmsSmsColumns.java
index ad2a172c36021f5bcc25f2bbb4537d7ba047e524..3f2c4c77433c85da15c1a0486a793faef4c33211 100644
--- a/src/org/smssecure/smssecure/database/MmsSmsColumns.java
+++ b/src/org/smssecure/smssecure/database/MmsSmsColumns.java
@@ -38,7 +38,6 @@ public interface MmsSmsColumns {
// Message attributes
protected static final long MESSAGE_ATTRIBUTE_MASK = 0xE0;
- protected static final long MESSAGE_FORCE_SMS_BIT = 0x40;
// Key Exchange Information
protected static final long KEY_EXCHANGE_MASK = 0xFF00;
@@ -53,7 +52,7 @@ public interface MmsSmsColumns {
// Secure Message Information
protected static final long SECURE_MESSAGE_BIT = 0x800000;
protected static final long END_SESSION_BIT = 0x400000;
- protected static final long PUSH_MESSAGE_BIT = 0x200000;
+ protected static final long XMPP_MESSAGE_BIT = 0x200000;
// Group Message Information
protected static final long GROUP_UPDATE_BIT = 0x10000;
@@ -89,10 +88,6 @@ public interface MmsSmsColumns {
return false;
}
- public static boolean isForcedSms(long type) {
- return (type & MESSAGE_FORCE_SMS_BIT) != 0;
- }
-
public static boolean isPendingMessageType(long type) {
return
(type & BASE_TYPE_MASK) == BASE_OUTBOX_TYPE ||
@@ -120,10 +115,6 @@ public interface MmsSmsColumns {
return (type & SECURE_MESSAGE_BIT) != 0;
}
- public static boolean isPushType(long type) {
- return (type & PUSH_MESSAGE_BIT) != 0;
- }
-
public static boolean isEndSessionType(long type) {
return (type & END_SESSION_BIT) != 0;
}
@@ -194,6 +185,10 @@ public interface MmsSmsColumns {
return (type & ENCRYPTION_REMOTE_LEGACY_BIT) != 0;
}
+ public static boolean isXmppType(long type) {
+ return (type & XMPP_MESSAGE_BIT) != 0;
+ }
+
public static boolean isXmppExchangeType(long type) {
return (type & XMPP_EXCHANGE_BIT) != 0;
}
diff --git a/src/org/smssecure/smssecure/database/RecipientPreferenceDatabase.java b/src/org/smssecure/smssecure/database/RecipientPreferenceDatabase.java
index e960ca21a04c430e2444fc42731c062298e4d633..0146e64b2e5ae6669b0b3da086eeaa332871e457 100644
--- a/src/org/smssecure/smssecure/database/RecipientPreferenceDatabase.java
+++ b/src/org/smssecure/smssecure/database/RecipientPreferenceDatabase.java
@@ -32,6 +32,7 @@ public class RecipientPreferenceDatabase extends Database {
private static final String MUTE_UNTIL = "mute_until";
private static final String COLOR = "color";
private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id";
+ public static final String XMPP_JID = "xmpp_jid";
public enum VibrateState {
DEFAULT(0), ENABLED(1), DISABLED(2);
@@ -60,7 +61,8 @@ public class RecipientPreferenceDatabase extends Database {
VIBRATE + " INTEGER DEFAULT " + VibrateState.DEFAULT.getId() + ", " +
MUTE_UNTIL + " INTEGER DEFAULT 0, " +
COLOR + " TEXT DEFAULT NULL, " +
- DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1);";
+ DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
+ XMPP_JID + " TEXT DEFAULT NULL);";
public RecipientPreferenceDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
@@ -95,6 +97,7 @@ public class RecipientPreferenceDatabase extends Database {
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
Uri notificationUri = notification == null ? null : Uri.parse(notification);
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
+ String xmppJid = cursor.getString(cursor.getColumnIndexOrThrow(XMPP_JID));
MaterialColor color;
@@ -109,7 +112,8 @@ public class RecipientPreferenceDatabase extends Database {
return Optional.of(new RecipientsPreferences(blocked, muteUntil,
VibrateState.fromId(vibrateState),
- notificationUri, color, defaultSubscriptionId));
+ notificationUri, color, defaultSubscriptionId,
+ xmppJid));
}
return Optional.absent();
@@ -124,6 +128,12 @@ public class RecipientPreferenceDatabase extends Database {
updateOrInsert(recipients, values);
}
+ public void setXmppJid(Recipients recipients, @Nullable String xmppJid) {
+ ContentValues values = new ContentValues();
+ values.put(XMPP_JID, xmppJid.equals("NULL") ? null : xmppJid);
+ updateOrInsert(recipients, values);
+ }
+
public void setDefaultSubscriptionId(@NonNull Recipients recipients, int defaultSubscriptionId) {
ContentValues values = new ContentValues();
values.put(DEFAULT_SUBSCRIPTION_ID, defaultSubscriptionId);
@@ -182,12 +192,14 @@ public class RecipientPreferenceDatabase extends Database {
private final Uri notification;
private final MaterialColor color;
private final int defaultSubscriptionId;
+ private final String xmppJid;
public RecipientsPreferences(boolean blocked, long muteUntil,
@NonNull VibrateState vibrateState,
@Nullable Uri notification,
@Nullable MaterialColor color,
- int defaultSubscriptionId)
+ int defaultSubscriptionId,
+ @Nullable String xmppJid)
{
this.blocked = blocked;
this.muteUntil = muteUntil;
@@ -195,12 +207,17 @@ public class RecipientPreferenceDatabase extends Database {
this.notification = notification;
this.color = color;
this.defaultSubscriptionId = defaultSubscriptionId;
+ this.xmppJid = xmppJid;
}
public @Nullable MaterialColor getColor() {
return color;
}
+ public @Nullable String getXmppJid() {
+ return xmppJid;
+ }
+
public boolean isBlocked() {
return blocked;
}
diff --git a/src/org/smssecure/smssecure/database/SmsDatabase.java b/src/org/smssecure/smssecure/database/SmsDatabase.java
index bb02df0e61d21ca3933f34eff6325f436b199c89..07d18cb6913108cd6ecac226303dba033afdb118 100644
--- a/src/org/smssecure/smssecure/database/SmsDatabase.java
+++ b/src/org/smssecure/smssecure/database/SmsDatabase.java
@@ -72,13 +72,14 @@ public class SmsDatabase extends MessagingDatabase {
public static final String REPLY_PATH_PRESENT = "reply_path_present";
public static final String SUBJECT = "subject";
public static final String SERVICE_CENTER = "service_center";
+ public static final String XMPP_ID = "xmpp_id";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " integer PRIMARY KEY, " +
THREAD_ID + " INTEGER, " + ADDRESS + " TEXT, " + ADDRESS_DEVICE_ID + " INTEGER DEFAULT 1, " + PERSON + " INTEGER, " +
DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " +
STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " +
DATE_DELIVERY_RECEIVED + " INTEGER DEFAULT 0," + SUBJECT + " TEXT, " + BODY + " TEXT, " +
- MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + SERVICE_CENTER + " TEXT, " + SUBSCRIPTION_ID + " INTEGER DEFAULT -1);";
+ MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + SERVICE_CENTER + " TEXT, " + SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + XMPP_ID + " TEXT DEFAULT NULL);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@@ -142,6 +143,25 @@ public class SmsDatabase extends MessagingDatabase {
}
}
+ public long getMessageIdFromXmpp(String id) {
+ String sql = "SELECT " + ID + " FROM " + TABLE_NAME + " WHERE " + XMPP_ID + " = ?";
+ String[] sqlArgs = new String[] {id+""};
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+
+ Cursor cursor = null;
+
+ try {
+ cursor = db.rawQuery(sql, sqlArgs);
+ if (cursor != null && cursor.moveToFirst())
+ return cursor.getLong(0);
+ else
+ return -1;
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ }
+
public int getMessageCount() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
@@ -207,12 +227,8 @@ public class SmsDatabase extends MessagingDatabase {
updateTypeBitmask(id, Types.SECURE_MESSAGE_BIT, 0);
}
- public void markAsPush(long id) {
- updateTypeBitmask(id, 0, Types.PUSH_MESSAGE_BIT);
- }
-
- public void markAsForcedSms(long id) {
- updateTypeBitmask(id, Types.PUSH_MESSAGE_BIT, Types.MESSAGE_FORCE_SMS_BIT);
+ public void markAsXmpp(long id) {
+ updateTypeBitmask(id, 0, Types.XMPP_MESSAGE_BIT);
}
public void markAsXmppExchange(long id) {
@@ -281,6 +297,14 @@ public class SmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId);
}
+ public void updateXmppId(long id, String xmppId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(XMPP_ID, xmppId);
+
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {id+""});
+ }
+
public void markAsSentFailed(long id) {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_FAILED_TYPE);
}
@@ -373,7 +397,7 @@ public class SmsDatabase extends MessagingDatabase {
// type |= Types.ENCRYPTION_REMOTE_BIT;
}
- if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT;
+ if (message.isXmpp()) type |= Types.XMPP_MESSAGE_BIT;
Recipients recipients;
@@ -437,12 +461,12 @@ public class SmsDatabase extends MessagingDatabase {
}
protected long insertMessageOutbox(long threadId, OutgoingTextMessage message,
- long type, boolean forceSms, long date)
+ long type, long date)
{
if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT;
else if (message.isSecureMessage()) type |= Types.SECURE_MESSAGE_BIT;
+ else if (message.isXmppExchange()) type |= Types.XMPP_EXCHANGE_BIT;
else if (message.isEndSession()) type |= Types.END_SESSION_BIT;
- if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
ContentValues contentValues = new ContentValues(7);
contentValues.put(ADDRESS, PhoneNumberUtils.formatNumber(message.getRecipients().getPrimaryRecipient().getNumber()));
diff --git a/src/org/smssecure/smssecure/database/model/MessageRecord.java b/src/org/smssecure/smssecure/database/model/MessageRecord.java
index 71d05d4969a6430446f0092f75af369f466faeaf..4ef86bb0affb7584f97442a9a33157b4eb03a0f8 100644
--- a/src/org/smssecure/smssecure/database/model/MessageRecord.java
+++ b/src/org/smssecure/smssecure/database/model/MessageRecord.java
@@ -107,19 +107,11 @@ public abstract class MessageRecord extends DisplayRecord {
return id;
}
- public boolean isPush() {
- return SmsDatabase.Types.isPushType(type) && !SmsDatabase.Types.isForcedSms(type);
- }
-
public long getTimestamp() {
if (SilencePreferences.showSentTime(context)) return getDateSent();
else return getDateReceived();
}
- public boolean isForcedSms() {
- return SmsDatabase.Types.isForcedSms(type);
- }
-
public boolean isStaleKeyExchange() {
return SmsDatabase.Types.isStaleKeyExchange(type);
}
@@ -160,6 +152,10 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isXmppExchangeType(type);
}
+ public boolean isXmpp() {
+ return SmsDatabase.Types.isXmppType(type);
+ }
+
public Recipient getIndividualRecipient() {
return individualRecipient;
}
diff --git a/src/org/smssecure/smssecure/database/model/SmsMessageRecord.java b/src/org/smssecure/smssecure/database/model/SmsMessageRecord.java
index af3cce6424ccad4ed238f22a4d795a1b0dcc32fc..52722f5655fe7d39349f0981a1c0037af5d0fc8f 100644
--- a/src/org/smssecure/smssecure/database/model/SmsMessageRecord.java
+++ b/src/org/smssecure/smssecure/database/model/SmsMessageRecord.java
@@ -72,7 +72,7 @@ public class SmsMessageRecord extends MessageRecord {
} else if (isInvalidVersionKeyExchange()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_key_exchange_message_for_invalid_protocol_version));
} else if (isXmppExchange()) {
- return emphasisAdded(context.getString(R.string.ConversationItem_xmpp_address_update_silence));
+ return emphasisAdded(context.getString(R.string.ConversationItem_xmpp_address));
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
} else if (isBundleKeyExchange()) {
diff --git a/src/org/smssecure/smssecure/database/model/ThreadRecord.java b/src/org/smssecure/smssecure/database/model/ThreadRecord.java
index 703cb9dbb0f545c62c414619095d1a09a6e9c5ec..e6d4dfef7f98ebaff7d0aa539be2003a8e73a545 100644
--- a/src/org/smssecure/smssecure/database/model/ThreadRecord.java
+++ b/src/org/smssecure/smssecure/database/model/ThreadRecord.java
@@ -77,7 +77,7 @@ public class ThreadRecord extends DisplayRecord {
} else if (isDuplicateMessageType()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
} else if (isXmppExchange()) {
- return emphasisAdded(context.getString(R.string.ConversationItem_xmpp_address_update_silence));
+ return emphasisAdded(context.getString(R.string.ConversationItem_xmpp_address));
} else if (SmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
diff --git a/src/org/smssecure/smssecure/jobs/MmsSendJob.java b/src/org/smssecure/smssecure/jobs/MmsSendJob.java
index 4af580263288257ee8beb910cbb679ade242380b..cf5b3b580da158954981134c42c108f79401f2f5 100644
--- a/src/org/smssecure/smssecure/jobs/MmsSendJob.java
+++ b/src/org/smssecure/smssecure/jobs/MmsSendJob.java
@@ -22,6 +22,7 @@ import org.smssecure.smssecure.notifications.MessageNotifier;
import org.smssecure.smssecure.recipients.Recipient;
import org.smssecure.smssecure.recipients.Recipients;
import org.smssecure.smssecure.recipients.RecipientFormattingException;
+import org.smssecure.smssecure.service.XmppService;
import org.smssecure.smssecure.transport.InsecureFallbackApprovalException;
import org.smssecure.smssecure.transport.UndeliverableMessageException;
import org.smssecure.smssecure.util.Hex;
@@ -79,36 +80,52 @@ public class MmsSendJob extends SendJob {
OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId);
try {
+
boolean upgradedSecure = false;
SendReq pdu = constructSendPdu(masterSecret, message);
- if (message.isSecure()) {
- Log.w(TAG, "Encrypting MMS...");
+ if (message.isXmpp() || message.isSecure()) {
+ Log.w(TAG, "Encrypting media message...");
pdu = getEncryptedMessage(masterSecret, pdu);
upgradedSecure = true;
}
validateDestinations(message, pdu);
- final byte[] pduBytes = getPduBytes(masterSecret, pdu);
- final SendConf sendConf = new CompatMmsConnection(context).send(pduBytes, message.getSubscriptionId());
- final MmsSendResult result = getSendResult(sendConf, pdu, upgradedSecure);
+ final byte[] pduBytes = getPduBytes(masterSecret, pdu);
+
+ if (!message.isXmpp()) {
+
+ final SendConf sendConf = new CompatMmsConnection(context).send(pduBytes, message.getSubscriptionId());
+ final MmsSendResult result = getSendResult(sendConf, pdu, upgradedSecure);
+
+ if (result.isUpgradedSecure()) {
+ database.markAsSecure(messageId);
+ }
+
+ database.markAsSent(messageId);
+ markAttachmentsUploaded(messageId, message.getAttachments());
+ } else {
+ Log.w(TAG, "Sending XMPP media message...");
- if (result.isUpgradedSecure()) {
database.markAsSecure(messageId);
- }
- database.markAsSent(messageId);
- markAttachmentsUploaded(messageId, message.getAttachments());
+ XmppService xmppService = XmppService.getInstance();
+ if (xmppService == null) throw new UndeliverableMessageException("XmppService not available!");
+ for (Attachment attachement : message.getAttachments()) {
+ Log.w(TAG, "Sending an attachement...");
+ xmppService.sendMedia(masterSecret, message.getBody(), attachement, messageId, message.getRecipients(), pduBytes);
+ }
+ }
} catch (UndeliverableMessageException | IOException e) {
Log.w(TAG, e);
database.markAsSentFailed(messageId);
- notifyMediaMessageDeliveryFailed(context, messageId);
+ MmsSendJob.notifyMediaMessageDeliveryFailed(context, messageId);
} catch (InsecureFallbackApprovalException e) {
Log.w(TAG, e);
database.markAsPendingInsecureSmsFallback(messageId);
- notifyMediaMessageDeliveryFailed(context, messageId);
+ MmsSendJob.notifyMediaMessageDeliveryFailed(context, messageId);
}
}
@@ -120,7 +137,7 @@ public class MmsSendJob extends SendJob {
@Override
public void onCanceled() {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
- notifyMediaMessageDeliveryFailed(context, messageId);
+ MmsSendJob.notifyMediaMessageDeliveryFailed(context, messageId);
}
private byte[] getPduBytes(MasterSecret masterSecret, SendReq message)
@@ -247,7 +264,7 @@ public class MmsSendJob extends SendJob {
return sendReq;
}
- private void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
+ public static void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
diff --git a/src/org/smssecure/smssecure/jobs/SmsDecryptJob.java b/src/org/smssecure/smssecure/jobs/SmsDecryptJob.java
index 6762ae9b891b1154afc26172004a2e26344de98d..f791c6b60fee5902170b4afe9360b53a7eab675c 100644
--- a/src/org/smssecure/smssecure/jobs/SmsDecryptJob.java
+++ b/src/org/smssecure/smssecure/jobs/SmsDecryptJob.java
@@ -16,7 +16,10 @@ import org.smssecure.smssecure.database.NoSuchMessageException;
import org.smssecure.smssecure.database.model.SmsMessageRecord;
import org.smssecure.smssecure.jobs.requirements.MasterSecretRequirement;
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.service.XmppService;
import org.smssecure.smssecure.sms.IncomingEncryptedMessage;
import org.smssecure.smssecure.sms.IncomingEndSessionMessage;
import org.smssecure.smssecure.sms.IncomingKeyExchangeMessage;
@@ -25,6 +28,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.sms.OutgoingTextMessage;
+import org.smssecure.smssecure.sms.OutgoingXmppExchangeMessage;
import org.smssecure.smssecure.util.SilencePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libaxolotl.DuplicateMessageException;
@@ -157,6 +162,8 @@ public class SmsDecryptJob extends MasterSecretJob {
if (SilencePreferences.isAutoRespondKeyExchangeEnabled(context) || manualOverride) {
try {
+ final int subscriptionId = -1; // Is there a way to get the selected SIM in conversation?
+
SmsCipher cipher = new SmsCipher(new SilenceAxolotlStore(context, masterSecret));
OutgoingKeyExchangeMessage response = cipher.process(context, message);
@@ -165,7 +172,18 @@ public class SmsDecryptJob extends MasterSecretJob {
SecurityEvent.broadcastSecurityUpdateEvent(context, threadId);
if (response != null) {
- MessageSender.send(context, masterSecret, response, threadId, true);
+ MessageSender.send(context, masterSecret, response, threadId, false);
+ }
+
+ if (SilencePreferences.isXmppRegistered(context)) {
+ Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSender(), false);
+
+ OutgoingXmppExchangeMessage xmppExchangeMessage =
+ new OutgoingXmppExchangeMessage(new OutgoingTextMessage(recipients,
+ SilencePreferences.getXmppUsername(context) + "@" + SilencePreferences.getXmppHostname(context),
+ subscriptionId));
+
+ MessageSender.send(context, masterSecret, xmppExchangeMessage, -1, false);
}
} catch (InvalidVersionException e) {
Log.w(TAG, e);
@@ -190,7 +208,16 @@ public class SmsDecryptJob extends MasterSecretJob {
throws NoSessionException, DuplicateMessageException, InvalidMessageException, LegacyMessageException
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
+
+ SmsCipher cipher = new SmsCipher(new SilenceAxolotlStore(context, masterSecret));
+ IncomingTextMessage xmppJid = cipher.decrypt(context, message);
+
database.markAsXmppExchange(messageId);
+
+ XmppService xmppService = XmppService.getInstance();
+ if (xmppService != null) xmppService.subscribe(xmppJid.getMessageBody());
+
+ SecurityEvent.broadcastSecurityUpdateEvent(context, threadId);
}
private String getAsymmetricDecryptedBody(MasterSecret masterSecret, String body)
diff --git a/src/org/smssecure/smssecure/jobs/SmsReceiveJob.java b/src/org/smssecure/smssecure/jobs/SmsReceiveJob.java
index 0396fc7a179dddf70645d8aed11eeef9ad4bb0b8..09d4232acd9fbf757b3d34c302a7fbf3c20b13b0 100644
--- a/src/org/smssecure/smssecure/jobs/SmsReceiveJob.java
+++ b/src/org/smssecure/smssecure/jobs/SmsReceiveJob.java
@@ -13,6 +13,7 @@ import org.smssecure.smssecure.database.EncryptingSmsDatabase;
import org.smssecure.smssecure.notifications.MessageNotifier;
import org.smssecure.smssecure.protocol.WirePrefix;
import org.smssecure.smssecure.recipients.RecipientFactory;
+import org.smssecure.smssecure.recipients.Recipient;
import org.smssecure.smssecure.recipients.Recipients;
import org.smssecure.smssecure.service.KeyCachingService;
import org.smssecure.smssecure.sms.IncomingTextMessage;
@@ -33,6 +34,8 @@ public class SmsReceiveJob extends ContextJob {
private final Object[] pdus;
private final int subscriptionId;
+ private final String body;
+ private final String senderNumber;
public SmsReceiveJob(Context context, Object[] pdus, int subscriptionId) {
super(context, JobParameters.newBuilder()
@@ -42,6 +45,19 @@ public class SmsReceiveJob extends ContextJob {
this.pdus = pdus;
this.subscriptionId = subscriptionId;
+ this.body = null;
+ this.senderNumber = null;
+ }
+
+ public SmsReceiveJob(Context context, String body, String senderNumber, int subscriptionId) {
+ super(context, JobParameters.newBuilder()
+ .withPersistence()
+ .withWakeLock(true)
+ .create());
+ this.pdus = null;
+ this.subscriptionId = subscriptionId;
+ this.body = body;
+ this.senderNumber = senderNumber;
}
@Override
@@ -50,7 +66,18 @@ public class SmsReceiveJob extends ContextJob {
@Override
public void onRun() {
MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
- Optional message = assembleMessageFragments(pdus, subscriptionId, masterSecret);
+ Optional message;
+
+ if (pdus != null) {
+ message = assembleMessageFragments(pdus, subscriptionId, masterSecret);
+ } else {
+ IncomingTextMessage incomingTextMessage = new IncomingTextMessage(senderNumber, 1, System.currentTimeMillis(), body, masterSecret == null);
+ if (WirePrefix.isPrefixedMessage(incomingTextMessage.getMessageBody())) {
+ message = Optional.fromNullable(multipartMessageHandler.processPotentialMultipartMessage(incomingTextMessage));
+ } else {
+ message = Optional.of(incomingTextMessage);
+ }
+ }
if (message.isPresent() && !isBlocked(message.get())) {
Pair messageAndThreadId = storeMessage(message.get());
diff --git a/src/org/smssecure/smssecure/jobs/SmsSendJob.java b/src/org/smssecure/smssecure/jobs/SmsSendJob.java
index af5e10b2fc94c53395845d93cee3a93e6361b5bc..04a455d3abe232c2ea02ed63f3a94801a7d0ea8a 100644
--- a/src/org/smssecure/smssecure/jobs/SmsSendJob.java
+++ b/src/org/smssecure/smssecure/jobs/SmsSendJob.java
@@ -16,6 +16,7 @@ import org.smssecure.smssecure.database.DatabaseFactory;
import org.smssecure.smssecure.database.EncryptingSmsDatabase;
import org.smssecure.smssecure.database.NoSuchMessageException;
import org.smssecure.smssecure.database.SmsDatabase;
+import org.smssecure.smssecure.database.MmsSmsColumns;
import org.smssecure.smssecure.database.model.SmsMessageRecord;
import org.smssecure.smssecure.jobs.requirements.MasterSecretRequirement;
import org.smssecure.smssecure.jobs.requirements.NetworkOrServiceRequirement;
@@ -23,6 +24,7 @@ import org.smssecure.smssecure.jobs.requirements.ServiceRequirement;
import org.smssecure.smssecure.notifications.MessageNotifier;
import org.smssecure.smssecure.recipients.Recipients;
import org.smssecure.smssecure.service.SmsDeliveryListener;
+import org.smssecure.smssecure.service.XmppService;
import org.smssecure.smssecure.sms.MultipartSmsMessageHandler;
import org.smssecure.smssecure.sms.OutgoingTextMessage;
import org.smssecure.smssecure.transport.InsecureFallbackApprovalException;
@@ -32,6 +34,7 @@ import org.smssecure.smssecure.util.SilencePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libaxolotl.NoSessionException;
+import java.lang.StringBuilder;
import java.util.ArrayList;
public class SmsSendJob extends SendJob {
@@ -91,7 +94,7 @@ public class SmsSendJob extends SendJob {
private void deliver(MasterSecret masterSecret, SmsMessageRecord message)
throws UndeliverableMessageException, InsecureFallbackApprovalException
{
- if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) {
+ if (message.isSecure() || message.isKeyExchange() || message.isEndSession() || message.isXmppExchange() || message.isXmpp()) {
deliverSecureMessage(masterSecret, message);
} else {
deliverPlaintextMessage(message);
@@ -101,38 +104,55 @@ public class SmsSendJob extends SendJob {
private void deliverSecureMessage(MasterSecret masterSecret, SmsMessageRecord message)
throws UndeliverableMessageException, InsecureFallbackApprovalException
{
- MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
- OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
-
- if (message.isSecure() || message.isEndSession()) {
+ OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
+ if (message.isSecure() || message.isEndSession() || message.isXmppExchange()) {
transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage);
}
- ArrayList messages = multipartMessageHandler.divideMessage(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());
+ MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
+ ArrayList messages = multipartMessageHandler.divideMessage(transportMessage, message.isXmpp());
+
+ if (!message.isXmpp()) {
+ 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());
+
+ for (int i=0;i attachments,
+ long sentTimeMillis,
+ int distributionType)
+ {
+ super(recipients, body, attachments, sentTimeMillis, -1, distributionType);
+ }
+
+ public OutgoingXmppMediaMessage(OutgoingMediaMessage base) {
+ super(base);
+ }
+
+ @Override
+ public boolean isSecure() {
+ return true;
+ }
+
+ @Override
+ public boolean isXmpp() {
+ return true;
+ }
+}
diff --git a/src/org/smssecure/smssecure/notifications/WearReplyReceiver.java b/src/org/smssecure/smssecure/notifications/WearReplyReceiver.java
index c85c18d5d2d3ebb90095465496ee2abbf1194045..4463788aeec0c4e8d8ddbdcbda06b25c45475d31 100644
--- a/src/org/smssecure/smssecure/notifications/WearReplyReceiver.java
+++ b/src/org/smssecure/smssecure/notifications/WearReplyReceiver.java
@@ -74,7 +74,7 @@ public class WearReplyReceiver extends MasterSecretBroadcastReceiver {
threadId = MessageSender.send(context, masterSecret, reply, -1, false);
} else {
OutgoingTextMessage reply = new OutgoingTextMessage(recipients, responseText.toString(), subscriptionId);
- threadId = MessageSender.send(context, masterSecret, reply, -1, false);
+ threadId = MessageSender.send(context, masterSecret, reply, -1);
}
DatabaseFactory.getThreadDatabase(context).setRead(threadId);
diff --git a/src/org/smssecure/smssecure/preferences/ChatsPreferenceFragment.java b/src/org/smssecure/smssecure/preferences/ChatsPreferenceFragment.java
index cd3e5e32e7a099e03e95a874ef26aff792a16bc2..38e4336ab1d331130dc11e223046178e88001e03 100644
--- a/src/org/smssecure/smssecure/preferences/ChatsPreferenceFragment.java
+++ b/src/org/smssecure/smssecure/preferences/ChatsPreferenceFragment.java
@@ -8,7 +8,6 @@ import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.support.v4.preference.PreferenceFragment;
import android.support.v7.app.AlertDialog;
-import android.text.TextUtils;
import android.util.Log;
import org.smssecure.smssecure.ApplicationPreferencesActivity;
diff --git a/src/org/smssecure/smssecure/preferences/XmppPreferenceFragment.java b/src/org/smssecure/smssecure/preferences/XmppPreferenceFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..5a96ea737ea14be7c708dc26900c5da3dc31c8c2
--- /dev/null
+++ b/src/org/smssecure/smssecure/preferences/XmppPreferenceFragment.java
@@ -0,0 +1,207 @@
+package org.smssecure.smssecure.preferences;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Looper;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.support.v4.preference.PreferenceFragment;
+import android.support.v7.app.AlertDialog;
+import android.util.Log;
+import android.widget.Toast;
+
+import org.smssecure.smssecure.ApplicationPreferencesActivity;
+import org.smssecure.smssecure.components.SwitchPreferenceCompat;
+import org.smssecure.smssecure.crypto.MasterSecret;
+import org.smssecure.smssecure.database.RecipientPreferenceDatabase.RecipientsPreferences;
+import org.smssecure.smssecure.R;
+import org.smssecure.smssecure.recipients.RecipientFactory;
+import org.smssecure.smssecure.recipients.Recipient;
+import org.smssecure.smssecure.recipients.Recipients;
+import org.smssecure.smssecure.service.KeyCachingService;
+import org.smssecure.smssecure.service.XmppService;
+import org.smssecure.smssecure.sms.MessageSender;
+import org.smssecure.smssecure.sms.OutgoingTextMessage;
+import org.smssecure.smssecure.sms.OutgoingXmppExchangeMessage;
+import org.smssecure.smssecure.util.SilencePreferences;
+import org.smssecure.smssecure.util.task.ProgressDialogAsyncTask;
+import org.smssecure.smssecure.util.XmppUtil;
+import org.smssecure.smssecure.XmppRegisterActivity;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class XmppPreferenceFragment extends PreferenceFragment {
+
+ private static final String TAG = XmppPreferenceFragment.class.getSimpleName();
+
+ private static final int REGISTERING_ACTIVITY_RESULT_CODE = 666;
+
+ private BroadcastReceiver xmppUpdateReceiver;
+
+ @Override
+ public void onCreate(Bundle paramBundle) {
+ super.onCreate(paramBundle);
+ addPreferencesFromResource(R.xml.preferences_xmpp);
+
+ setXmppUiSettings();
+
+ findPreference(SilencePreferences.XMPP_ENABLED_PREF)
+ .setOnPreferenceChangeListener(new RegisterXmppListener());
+
+ findPreference(SilencePreferences.XMPP_NOTIFY_CONTACTS)
+ .setOnPreferenceClickListener(new XmppNotifyContactsListener());
+
+ xmppUpdateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ setXmppUiSettings();
+ }
+ };
+
+ getActivity().registerReceiver(xmppUpdateReceiver, new IntentFilter(XmppService.XMPP_CONNECTIVITY_EVENT));
+
+ }
+
+ private static int getUiString(Context context) {
+ if (SilencePreferences.isXmppRegistered(context)) {
+ return XmppService.getInstance().isConnected() ?
+ R.string.preferences__xmpp_status_registered_connected :
+ R.string.preferences__xmpp_status_registered_disconnected;
+ } else {
+ return R.string.preferences__xmpp_status_unregistered;
+ }
+ }
+
+ private void setXmppUiSettings() {
+ Context context = getActivity();
+
+ if (!SilencePreferences.isXmppRegistered(context)) {
+ ((SwitchPreferenceCompat) findPreference(SilencePreferences.XMPP_ENABLED_PREF)).setChecked(false);
+ }
+
+ findPreference(SilencePreferences.XMPP_STATUS).setSummary(XmppPreferenceFragment.getUiString(context));
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ((ApplicationPreferencesActivity)getActivity()).getSupportActionBar().setTitle(R.string.preferences__xmpp);
+ }
+
+ private class RegisterXmppListener implements Preference.OnPreferenceChangeListener {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ final Context context = (Context) getActivity();
+
+ if (!SilencePreferences.isXmppRegistered(context)) {
+ startActivityForResult(new Intent(getActivity(), XmppRegisterActivity.class), REGISTERING_ACTIVITY_RESULT_CODE);
+ } else {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.preferences__xmpp_unregistering_xmpp);
+ builder.setMessage(R.string.preferences__xmpp_unregistering_from_xmpp_will_delete_the_account_on_the_server);
+ builder.setIconAttribute(R.attr.dialog_alert_icon);
+ builder.setCancelable(false);
+ builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ((SwitchPreferenceCompat) findPreference(SilencePreferences.XMPP_ENABLED_PREF)).setChecked(true);
+ }
+ });
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ XmppUtil.sendNullXmppMessage(context);
+ XmppService.getInstance().deleteAccount();
+ findPreference(SilencePreferences.XMPP_STATUS).setSummary(R.string.preferences__xmpp_status_unregistered);
+ SilencePreferences.setXmppUsername(context, "");
+ SilencePreferences.setXmppPassword(context, "");
+ SilencePreferences.setXmppHostname(context, "");
+ SilencePreferences.setXmppPort(context, 0);
+ new Thread() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ Toast.makeText(context.getApplicationContext(),
+ context.getString(R.string.XmppRegisterActivity__unregistered),
+ Toast.LENGTH_LONG).show();
+ Looper.loop();
+ }
+ }.start();
+ }
+ });
+ builder.show();
+ }
+ return true;
+ }
+ }
+
+ public static CharSequence getSummary(Context context) {
+ return context.getString(getUiString(context));
+ }
+
+ private class XmppNotifyContactsTask extends ProgressDialogAsyncTask {
+ private final Context context;
+
+ public XmppNotifyContactsTask(final Context context) {
+ super(context,
+ context.getString(R.string.preferences__xmpp_notifying),
+ context.getString(R.string.preferences__xmpp_sending_messages_to_contacts));
+ this.context = context;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ sendXmppExchangeMessages(context);
+ return null;
+ }
+ }
+
+ private class XmppNotifyContactsListener implements Preference.OnPreferenceClickListener {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ new XmppNotifyContactsTask(getActivity()).execute();
+
+ return true;
+ }
+ }
+
+ private void sendXmppExchangeMessages(final Context context) {
+ final MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
+
+ Recipients secureRecipients = RecipientFactory.getSecureRecipients(context, masterSecret, false);
+
+ Log.w(TAG, "Sending SMS message to: " + secureRecipients.toShortString());
+
+ for (Recipient recipient : secureRecipients) {
+ final int subscriptionId = -1; // Is there a way to get the selected SIM in conversation?
+
+ Recipients recipients = RecipientFactory.getRecipientsFor(context, recipient, false);
+
+ OutgoingXmppExchangeMessage xmppExchangeMessage =
+ new OutgoingXmppExchangeMessage(new OutgoingTextMessage(recipients,
+ SilencePreferences.getXmppUsername(context) + "@" + SilencePreferences.getXmppHostname(context),
+ subscriptionId));
+
+ MessageSender.send(context, masterSecret, xmppExchangeMessage, -1, false);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REGISTERING_ACTIVITY_RESULT_CODE) {
+ setXmppUiSettings();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (xmppUpdateReceiver != null) getActivity().unregisterReceiver(xmppUpdateReceiver);
+ }
+}
diff --git a/src/org/smssecure/smssecure/providers/PersistentBlobProvider.java b/src/org/smssecure/smssecure/providers/PersistentBlobProvider.java
index 428f954eb1334a06d8f095b5eac56b3704b7477b..2390bff3f9b19971d14a0caf0cdcb64daf37a8e7 100644
--- a/src/org/smssecure/smssecure/providers/PersistentBlobProvider.java
+++ b/src/org/smssecure/smssecure/providers/PersistentBlobProvider.java
@@ -7,6 +7,7 @@ import android.content.UriMatcher;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.v4.content.FileProvider;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -107,8 +108,9 @@ public class PersistentBlobProvider {
}
public Uri createForExternal(@NonNull String mimeType) throws IOException {
- return Uri.fromFile(new File(getExternalDir(context),
- String.valueOf(System.currentTimeMillis()) + "." + getExtensionFromMimeType(mimeType)));
+ return FileProvider.getUriForFile(context,
+ "org.smssecure.provider.smssecure",
+ new File(getExternalDir(context), String.valueOf(System.currentTimeMillis()) + "." + getExtensionFromMimeType(mimeType)));
}
public boolean delete(@NonNull Uri uri) {
diff --git a/src/org/smssecure/smssecure/recipients/Recipient.java b/src/org/smssecure/smssecure/recipients/Recipient.java
index 9b366612db33336a3951ad80865ff17bfacc6145..f6cdac055201c6c8d2e56d6f30fffe9962a8b04d 100644
--- a/src/org/smssecure/smssecure/recipients/Recipient.java
+++ b/src/org/smssecure/smssecure/recipients/Recipient.java
@@ -51,6 +51,7 @@ public class Recipient {
private Uri contactUri;
@Nullable private MaterialColor color;
+ @Nullable private String xmppJid;
Recipient(long recipientId,
@NonNull String number,
@@ -61,12 +62,14 @@ public class Recipient {
this.number = number;
this.contactPhoto = ContactPhotoFactory.getLoadingPhoto();
this.color = null;
+ this.xmppJid = null;
if (stale != null) {
this.name = stale.name;
this.contactUri = stale.contactUri;
this.contactPhoto = stale.contactPhoto;
this.color = stale.color;
+ this.xmppJid = stale.xmppJid;
}
future.addListener(new FutureTaskListener() {
@@ -79,6 +82,7 @@ public class Recipient {
Recipient.this.contactUri = result.contactUri;
Recipient.this.contactPhoto = result.avatar;
Recipient.this.color = result.color;
+ Recipient.this.xmppJid = result.xmppJid;
}
notifyListeners();
@@ -99,6 +103,7 @@ public class Recipient {
this.name = details.name;
this.contactPhoto = details.avatar;
this.color = details.color;
+ this.xmppJid = details.xmppJid;
}
public synchronized @Nullable Uri getContactUri() {
@@ -123,6 +128,19 @@ public class Recipient {
notifyListeners();
}
+ public synchronized @Nullable String getXmppJid() {
+ return this.xmppJid;
+ }
+
+ public void setXmppJid(@Nullable String xmppJid) {
+ synchronized (this) {
+ if (xmppJid == null || xmppJid.equals("NULL")) xmppJid = null;
+ this.xmppJid = xmppJid;
+ }
+
+ notifyListeners();
+ }
+
public String getNumber() {
return number;
}
@@ -153,7 +171,7 @@ public class Recipient {
public static Recipient getUnknownRecipient() {
return new Recipient(-1, new RecipientDetails("Unknown", "Unknown", null,
- ContactPhotoFactory.getDefaultContactPhoto(null), null));
+ ContactPhotoFactory.getDefaultContactPhoto(null), null, null));
}
@Override
diff --git a/src/org/smssecure/smssecure/recipients/RecipientFactory.java b/src/org/smssecure/smssecure/recipients/RecipientFactory.java
index 5df37d2c77ef1e715ac6afc0ce901ca266421f93..d4aad831cab8c6e4e37a3a6c297b2c9123df6dc6 100644
--- a/src/org/smssecure/smssecure/recipients/RecipientFactory.java
+++ b/src/org/smssecure/smssecure/recipients/RecipientFactory.java
@@ -17,11 +17,18 @@
package org.smssecure.smssecure.recipients;
import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.smssecure.smssecure.contacts.avatars.ContactPhotoFactory;
+import org.smssecure.smssecure.crypto.MasterSecret;
+import org.smssecure.smssecure.crypto.SessionUtil;
import org.smssecure.smssecure.database.CanonicalAddressDatabase;
+import org.smssecure.smssecure.database.DatabaseFactory;
+import org.smssecure.smssecure.database.RecipientPreferenceDatabase;
import org.smssecure.smssecure.util.Util;
import org.whispersystems.libaxolotl.util.guava.Optional;
@@ -31,7 +38,9 @@ import java.util.StringTokenizer;
public class RecipientFactory {
- private static final RecipientProvider provider = new RecipientProvider();
+ private static final RecipientProvider provider = new RecipientProvider();
+ private static final String TABLE = "recipient_preferences";
+ private static final String RECIPIENT_IDS_COLUMN = "recipient_ids";
public static Recipients getRecipientsForIds(Context context, String recipientIds, boolean asynchronous) {
if (TextUtils.isEmpty(recipientIds))
@@ -95,6 +104,20 @@ public class RecipientFactory {
return getRecipientsForIds(context, ids, asynchronous);
}
+ public static @NonNull Recipients getRecipientsFromXmpp(@NonNull Context context, @NonNull List xmppAddresses, boolean asynchronous) {
+ List ids = new LinkedList<>();
+
+ for (String xmppAddress : xmppAddresses) {
+ Optional id = getRecipientIdFromXmpp(context, xmppAddress);
+
+ if (id.isPresent()) {
+ ids.add(String.valueOf(id.get()));
+ }
+ }
+
+ return getRecipientsForIds(context, ids, asynchronous);
+ }
+
private static @NonNull Recipients getRecipientsForIds(Context context, List idStrings, boolean asynchronous) {
long[] ids = new long[idStrings.size()];
int i = 0;
@@ -118,6 +141,78 @@ public class RecipientFactory {
return Optional.of(CanonicalAddressDatabase.getInstance(context).getCanonicalAddressId(number));
}
+ public static @NonNull Recipients getXmppRecipients(@NonNull Context context, boolean asynchronous) {
+ return getRecipientsForIds(context, getXmppRecipientsIds(context), asynchronous);
+ }
+
+ private static List getXmppRecipientsIds(Context context) {
+ Cursor cursor = null;
+ try {
+ DatabaseHelper databaseHelper = DatabaseHelper.getInstance(context);
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ String[] columns = new String[]{RECIPIENT_IDS_COLUMN};
+ String[] whereArgs = new String[]{"NULL"};
+ String where = RecipientPreferenceDatabase.XMPP_JID + "!=?";
+
+ cursor = db.query(TABLE, columns, where, whereArgs, null, null, null);
+
+ List recipients = new LinkedList<>();
+
+ while (cursor != null && cursor.moveToNext()) {
+ recipients.add(String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_IDS_COLUMN))));
+ }
+
+ return recipients;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private static Optional getRecipientIdFromXmpp(Context context, String xmppAddress) {
+ xmppAddress = xmppAddress.trim();
+
+ if (xmppAddress.isEmpty()) return Optional.absent();
+
+ Cursor cursor = null;
+
+ try {
+ DatabaseHelper databaseHelper = DatabaseHelper.getInstance(context);
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ String[] columns = new String[]{RECIPIENT_IDS_COLUMN};
+ String[] whereArgs = new String[]{xmppAddress};
+ String where = RecipientPreferenceDatabase.XMPP_JID + "=?";
+
+ cursor = db.query(TABLE, columns, where, whereArgs, null, null, null, "1");
+
+ if (cursor != null && cursor.moveToFirst()) {
+ return Optional.of(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_IDS_COLUMN)));
+ }
+
+ return Optional.absent();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ public static @NonNull Recipients getSecureRecipients(@NonNull Context context, @NonNull MasterSecret masterSecret, boolean asynchronous) {
+ List addresses = CanonicalAddressDatabase.getInstance(context).getCanonicalNumbers();
+ List secureAddresses = new LinkedList<>();
+
+ for (String address : addresses) {
+ Optional addressId = getRecipientIdFromNumber(context, address);
+ if (SessionUtil.hasSession(context, masterSecret, address) && addressId.isPresent()) secureAddresses.add(addressId.get());
+ }
+
+ long[] secureAddressesArray = new long[secureAddresses.size()];
+ for (int i = 0; i < secureAddresses.size(); i++) secureAddressesArray[i] = secureAddresses.get(i);
+
+ return getRecipientsForIds(context, secureAddressesArray, asynchronous);
+ }
+
private static boolean hasBracketedNumber(String recipient) {
int openBracketIndex = recipient.indexOf('<');
@@ -137,4 +232,29 @@ public class RecipientFactory {
provider.clearCache();
}
+ private static class DatabaseHelper extends SQLiteOpenHelper {
+
+ private static DatabaseHelper instance;
+
+ public static DatabaseHelper getInstance(Context context) {
+ if (instance == null) {
+ instance = new DatabaseHelper(context, DatabaseFactory.DATABASE_NAME, null, DatabaseFactory.DATABASE_VERSION);
+ }
+ return instance;
+ }
+
+ public DatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
+ super(context, name, factory, version);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ }
+
+ }
+
}
diff --git a/src/org/smssecure/smssecure/recipients/RecipientProvider.java b/src/org/smssecure/smssecure/recipients/RecipientProvider.java
index 3f079200ab1c2883156bc8fa99bd72f5e30f6e7b..a26ad2eb4644305599cd692079d5c3e528b21be7 100644
--- a/src/org/smssecure/smssecure/recipients/RecipientProvider.java
+++ b/src/org/smssecure/smssecure/recipients/RecipientProvider.java
@@ -67,7 +67,7 @@ public class RecipientProvider {
private static final Map STATIC_DETAILS = new HashMap() {{
put("262966", new RecipientDetails("Amazon", "262966", null,
ContactPhotoFactory.getDefaultGroupPhoto(),
- ContactColors.UNKNOWN_COLOR));
+ ContactColors.UNKNOWN_COLOR, null));
}};
Recipient getRecipient(Context context, long recipientId, boolean asynchronous) {
@@ -132,6 +132,7 @@ public class RecipientProvider {
private @NonNull RecipientDetails getIndividualRecipientDetails(Context context, long recipientId, @NonNull String number) {
Optional preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(new long[]{recipientId});
MaterialColor color = preferences.isPresent() ? preferences.get().getColor() : null;
+ String xmppJid = preferences.isPresent() ? preferences.get().getXmppJid() : null;
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION,
null, null, null);
@@ -146,7 +147,7 @@ public class RecipientProvider {
Uri.withAppendedPath(Contacts.CONTENT_URI, cursor.getLong(2) + ""),
name);
- return new RecipientDetails(cursor.getString(0), resultNumber, contactUri, contactPhoto, color);
+ return new RecipientDetails(cursor.getString(0), resultNumber, contactUri, contactPhoto, color, xmppJid);
} else {
Log.w(TAG, "resultNumber is null");
}
@@ -157,7 +158,7 @@ public class RecipientProvider {
}
if (STATIC_DETAILS.containsKey(number)) return STATIC_DETAILS.get(number);
- else return new RecipientDetails(null, number, null, ContactPhotoFactory.getDefaultContactPhoto(null), color);
+ else return new RecipientDetails(null, number, null, ContactPhotoFactory.getDefaultContactPhoto(null), color, xmppJid);
}
private @NonNull RecipientDetails getGroupRecipientDetails(Context context, String groupId) {
@@ -167,13 +168,13 @@ public class RecipientProvider {
if (record != null) {
ContactPhoto contactPhoto = ContactPhotoFactory.getGroupContactPhoto(record.getAvatar());
- return new RecipientDetails(record.getTitle(), groupId, null, contactPhoto, null);
+ return new RecipientDetails(record.getTitle(), groupId, null, contactPhoto, null, null);
}
- return new RecipientDetails(null, groupId, null, ContactPhotoFactory.getDefaultGroupPhoto(), null);
+ return new RecipientDetails(null, groupId, null, ContactPhotoFactory.getDefaultGroupPhoto(), null, null);
} catch (IOException e) {
Log.w("RecipientProvider", e);
- return new RecipientDetails(null, groupId, null, ContactPhotoFactory.getDefaultGroupPhoto(), null);
+ return new RecipientDetails(null, groupId, null, ContactPhotoFactory.getDefaultGroupPhoto(), null, null);
}
}
@@ -202,16 +203,18 @@ public class RecipientProvider {
@NonNull public final ContactPhoto avatar;
@Nullable public final Uri contactUri;
@Nullable public final MaterialColor color;
+ @Nullable public final String xmppJid;
public RecipientDetails(@Nullable String name, @NonNull String number,
@Nullable Uri contactUri, @NonNull ContactPhoto avatar,
- @Nullable MaterialColor color)
+ @Nullable MaterialColor color, @Nullable String xmppJid)
{
this.name = name;
this.number = number;
this.avatar = avatar;
this.contactUri = contactUri;
this.color = color;
+ this.xmppJid = xmppJid;
}
}
@@ -274,4 +277,4 @@ public class RecipientProvider {
-}
\ No newline at end of file
+}
diff --git a/src/org/smssecure/smssecure/recipients/Recipients.java b/src/org/smssecure/smssecure/recipients/Recipients.java
index fbd2e85a961cb70caae3fb9b41bd9799a3825359..0db5866fe9949382028e3809763fd2db6af0c184 100644
--- a/src/org/smssecure/smssecure/recipients/Recipients.java
+++ b/src/org/smssecure/smssecure/recipients/Recipients.java
@@ -178,6 +178,19 @@ public class Recipients implements Iterable, RecipientModifiedListene
else if (!isEmpty()) recipients.get(0).setColor(color);
}
+ public synchronized @Nullable String getXmppJid() {
+ if (!isSingleRecipient() || isGroupRecipient() || isEmpty()){
+ return null;
+ } else {
+ return recipients.get(0).getXmppJid();
+ }
+ }
+
+ public synchronized void setXmppJid(@NonNull String xmppJid) {
+ if (!isSingleRecipient() || isGroupRecipient()) throw new AssertionError("Groups don't have unique JID!");
+ else if (!isEmpty()) recipients.get(0).setXmppJid(xmppJid);
+ }
+
public synchronized void addListener(RecipientsModifiedListener listener) {
if (listeners.isEmpty()) {
for (Recipient recipient : recipients) {
diff --git a/src/org/smssecure/smssecure/service/KeyCachingService.java b/src/org/smssecure/smssecure/service/KeyCachingService.java
index 56e82d559501385258ec0952e78482df41906fb5..78c22e087e817dac781935fb6d74db3af83a2389 100644
--- a/src/org/smssecure/smssecure/service/KeyCachingService.java
+++ b/src/org/smssecure/smssecure/service/KeyCachingService.java
@@ -249,7 +249,7 @@ public class KeyCachingService extends Service {
startForeground(SERVICE_RUNNING_ID, builder.build());
}
- private void foregroundServiceICS() {
+ private void foregroundServiceLegacy() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.key_caching_notification);
@@ -263,20 +263,6 @@ public class KeyCachingService extends Service {
startForeground(SERVICE_RUNNING_ID, builder.build());
}
- private void foregroundServiceLegacy() {
- Notification notification = new Notification(R.drawable.icon_cached,
- getString(R.string.KeyCachingService_silence_passphrase_cached),
- System.currentTimeMillis());
- notification.setLatestEventInfo(getApplicationContext(),
- getString(R.string.KeyCachingService_passphrase_cached),
- getString(R.string.KeyCachingService_silence_passphrase_cached),
- buildLaunchIntent());
- notification.tickerText = null;
-
- stopForeground(true);
- startForeground(SERVICE_RUNNING_ID, notification);
- }
-
private void foregroundService() {
if (SilencePreferences.isPasswordDisabled(this)) {
stopForeground(true);
@@ -285,8 +271,6 @@ public class KeyCachingService extends Service {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
foregroundServiceModern();
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
- foregroundServiceICS();
} else {
foregroundServiceLegacy();
}
diff --git a/src/org/smssecure/smssecure/service/QuickResponseService.java b/src/org/smssecure/smssecure/service/QuickResponseService.java
index e91d88721d491b3440fb0548d80467523c8fc75c..060574401a295ee556ec0ec9050c50116a193680 100644
--- a/src/org/smssecure/smssecure/service/QuickResponseService.java
+++ b/src/org/smssecure/smssecure/service/QuickResponseService.java
@@ -19,11 +19,14 @@ import org.smssecure.smssecure.recipients.Recipients;
import org.smssecure.smssecure.sms.MessageSender;
import org.smssecure.smssecure.sms.OutgoingTextMessage;
import org.smssecure.smssecure.util.Rfc5724Uri;
+import org.smssecure.smssecure.util.ViewUtil;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.net.URISyntaxException;
import java.net.URLDecoder;
+import static org.smssecure.smssecure.TransportOption.Type;
+
public class QuickResponseService extends MasterSecretIntentService {
private static final String TAG = QuickResponseService.class.getSimpleName();
@@ -59,7 +62,8 @@ public class QuickResponseService extends MasterSecretIntentService {
if (!TextUtils.isEmpty(content)) {
if (recipients.isSingleRecipient()) {
- MessageSender.send(this, masterSecret, new OutgoingTextMessage(recipients, content, subscriptionId), -1, false);
+ //boolean isXmpp = ViewUtil.findById(getApplicationContext(), R.id.send_button).getSelectedTransport().getType() == Type.SECURE_XMPP;
+ MessageSender.send(this, masterSecret, new OutgoingTextMessage(recipients, content, subscriptionId), -1);
} else {
MessageSender.send(this, masterSecret, new OutgoingMediaMessage(recipients, new SlideDeck(), content, System.currentTimeMillis(),
subscriptionId, ThreadDatabase.DistributionTypes.DEFAULT), -1, false);
diff --git a/src/org/smssecure/smssecure/service/XmppHttpUpload.java b/src/org/smssecure/smssecure/service/XmppHttpUpload.java
new file mode 100644
index 0000000000000000000000000000000000000000..19385c2f16ded1b67456563d192f2fb1a87e3016
--- /dev/null
+++ b/src/org/smssecure/smssecure/service/XmppHttpUpload.java
@@ -0,0 +1,113 @@
+package org.smssecure.smssecure.service;
+
+// http://stackoverflow.com/questions/35007157/how-to-handle-xmpp-xep-0363-http-file-upload-feature-at-client-side-using-smack
+
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.StanzaListener;
+import org.jivesoftware.smack.filter.StanzaFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Stanza;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.provider.IntrospectionProvider;
+import org.jivesoftware.smack.provider.ProviderManager;
+import org.jivesoftware.smack.tcp.XMPPTCPConnection;
+
+public class XmppHttpUpload implements StanzaListener{
+ private XMPPTCPConnection connection;
+ private String id;
+ private SlotGrantedListener listener;
+
+ static {
+ ProviderManager.addIQProvider("slot", "urn:xmpp:http:upload", new SlotIQProvider());
+ }
+
+ public static class SlotIQProvider extends IntrospectionProvider.IQIntrospectionProvider {
+ public SlotIQProvider() {
+ super(SlotIQ.class);
+ }
+ }
+
+ public static class SlotIQ extends IQ {
+ private String put;
+ private String get;
+
+ public SlotIQ() {
+ super("request", "urn:xmpp:http:upload");
+ }
+
+ public String getPut() {
+ return put;
+ }
+
+ public void setPut(String put) {
+ this.put = put;
+ }
+
+ public String getGet() {
+ return get;
+ }
+
+ public void setGet(String get) {
+ this.get = get;
+ }
+
+ @Override
+ protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
+ return null;
+ }
+ }
+
+ public interface SlotGrantedListener {
+ void slotGranted(String put, String get);
+ void slotDenied(String error);
+ }
+
+ public XmppHttpUpload(XMPPTCPConnection connection) {
+ this.connection = connection;
+ }
+
+ public void requestSlot(String to, final long messageId, final long fileSize, final String contentType, SlotGrantedListener listener)
+ throws SmackException.NotConnectedException
+ {
+ this.listener = listener;
+ final IQ iq = new IQ("request", "urn:xmpp:http:upload") {
+ @Override
+ protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
+ xml.rightAngleBracket();
+ xml.element("filename", String.valueOf(messageId));
+ xml.element("size", String.valueOf(fileSize));
+ xml.element("content-type", contentType);
+ return xml;
+ }
+ };
+ iq.setType(IQ.Type.get);
+ iq.setFrom(connection.getUser());
+ iq.setTo(to);
+
+ id = iq.getStanzaId();
+
+ connection.addAsyncStanzaListener(this, new StanzaFilter() {
+ @Override
+ public boolean accept(Stanza stanza) {
+ return id != null && id.equals(stanza.getStanzaId());
+ }
+ });
+
+ connection.sendStanza(iq);
+ }
+
+ @Override
+ public void processPacket(Stanza packet) throws SmackException.NotConnectedException {
+ connection.removeAsyncStanzaListener(this);
+ if (((IQ)packet).getType() != IQ.Type.error) {
+ SlotIQ slotIQ = (SlotIQ) packet;
+ listener.slotGranted(slotIQ.getPut(), slotIQ.getGet());
+ } else {
+ XMPPError error = packet.getError();
+ String errDesc = error.getDescriptiveText(null);
+ String errText = errDesc != null ? String.format("%s (%s)", errDesc, error.toString()) : error.toString();
+ listener.slotDenied(errText);
+ }
+ }
+
+}
diff --git a/src/org/smssecure/smssecure/service/XmppService.java b/src/org/smssecure/smssecure/service/XmppService.java
new file mode 100644
index 0000000000000000000000000000000000000000..408aefe767242d4d9a734141113a1da5ba058195
--- /dev/null
+++ b/src/org/smssecure/smssecure/service/XmppService.java
@@ -0,0 +1,683 @@
+package org.smssecure.smssecure.service;
+
+import android.app.Activity;
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Process;
+import android.os.SystemClock;
+import android.support.annotation.Nullable;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+
+import org.smssecure.smssecure.ApplicationContext;
+import org.smssecure.smssecure.attachments.Attachment;
+import org.smssecure.smssecure.BuildConfig;
+import org.smssecure.smssecure.crypto.MasterSecret;
+import org.smssecure.smssecure.crypto.SmsCipher;
+import org.smssecure.smssecure.crypto.storage.SilenceAxolotlStore;
+import org.smssecure.smssecure.database.DatabaseFactory;
+import org.smssecure.smssecure.database.EncryptingSmsDatabase;
+import org.smssecure.smssecure.database.MmsDatabase;
+import org.smssecure.smssecure.jobs.MmsSendJob;
+import org.smssecure.smssecure.jobs.SmsReceiveJob;
+import org.smssecure.smssecure.jobs.SmsSentJob;
+import org.smssecure.smssecure.R;
+import org.smssecure.smssecure.recipients.Recipient;
+import org.smssecure.smssecure.recipients.Recipients;
+import org.smssecure.smssecure.recipients.RecipientFactory;
+import org.smssecure.smssecure.service.SmsDeliveryListener;
+import org.smssecure.smssecure.service.XmppHttpUpload;
+import org.smssecure.smssecure.service.XmppHttpUpload.SlotGrantedListener;
+import org.smssecure.smssecure.sms.MultipartSmsMessageHandler;
+import org.smssecure.smssecure.sms.OutgoingEncryptedMessage;
+import org.smssecure.smssecure.sms.OutgoingTextMessage;
+import org.smssecure.smssecure.transport.UndeliverableMessageException;
+import org.smssecure.smssecure.util.Base64;
+import org.smssecure.smssecure.util.ServiceUtil;
+import org.smssecure.smssecure.util.SilencePreferences;
+import org.smssecure.smssecure.util.XmppUtil;
+import org.smssecure.smssecure.util.XmppUtil.XmppServer;
+
+import org.jivesoftware.smack.AbstractXMPPConnection;
+import org.jivesoftware.smack.chat.Chat;
+import org.jivesoftware.smack.chat.ChatManager;
+import org.jivesoftware.smack.chat.ChatManagerListener;
+import org.jivesoftware.smack.chat.ChatMessageListener;
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.ExtensionElement;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.Stanza;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.ReconnectionManager;
+import org.jivesoftware.smack.tcp.XMPPTCPConnection;
+import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
+import org.jivesoftware.smack.roster.Roster;
+import org.jivesoftware.smack.roster.RosterEntry;
+import org.jivesoftware.smack.roster.RosterListener;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
+import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.iqregister.AccountManager;
+import org.jivesoftware.smackx.ping.android.ServerPingWithAlarmManager;
+import org.jivesoftware.smackx.receipts.DeliveryReceipt;
+import org.jivesoftware.smackx.receipts.DeliveryReceiptManager;
+import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest;
+import org.jivesoftware.smackx.receipts.ReceiptReceivedListener;
+import org.jivesoftware.smackx.xdata.FormField;
+import org.jivesoftware.smackx.xdata.packet.DataForm;
+
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+import java.io.IOException;
+import java.lang.Runnable;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import java.util.List;
+
+public class XmppService extends Service {
+ private static final String TAG = XmppService.class.getSimpleName();
+
+ private static XmppService instance;
+ private HttpUploadService httpUploadService = null;
+
+ private Looper looper;
+ private ConfigureAndConnectHandler configureAndConnectHandler;
+
+ public static final int SERVICE_RUNNING_ID = 4142;
+ public static final String XMPP_CONNECTIVITY_EVENT = "org.smssecure.smssecure.XMPP_CONNECTIVITY_UPDATE";
+ private static final String USER_AGENT = "Silence/" + BuildConfig.VERSION_NAME;
+
+ public static Integer DISCONNECTED = 0;
+ public static Integer CONNECTED = 1;
+ public static Integer CONNECTING = 2;
+ private Integer connectionStatus = DISCONNECTED;
+ private boolean hasBeenConnected = false;
+
+ private static int RECONNECTION_DELAY = 10; // 10s
+ private static long IDLE = 1000000;
+ private static long XMPP_SEND_MEDIA_TIMEOUT = 120; // 2min
+
+ private XMPPTCPConnection connection;
+ private XmppServer registeredXmppServer;
+ private Roster roster;
+ private ReconnectionManager reconnectionManager;
+
+ private final class ConfigureAndConnectHandler extends Handler {
+ public ConfigureAndConnectHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(android.os.Message message) {
+ configureAndConnect();
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ Log.w(TAG, "onCreate()");
+ instance = this;
+
+ ServerPingWithAlarmManager.onCreate(getApplicationContext());
+ if (!SilencePreferences.isXmppRegistered(this)) {
+ Log.w(TAG, "Xmpp is disabled. Exiting...");
+ stopSelf();
+ } else {
+ HandlerThread thread = new HandlerThread("XmppConfigureAndConnectHandlerThread", Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+ configureAndConnectHandler = new ConfigureAndConnectHandler(thread.getLooper());
+ }
+
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.w(TAG, "onStartCommand(): received start id " + startId + ": " + intent);
+ if (SilencePreferences.isXmppRegistered(this)){
+ configureAndConnectHandler.sendMessage(configureAndConnectHandler.obtainMessage());
+ }
+
+ return START_STICKY;
+ }
+
+ private void configureAndConnect() {
+ XmppService instance = this;
+ instance.registeredXmppServer = XmppUtil.getRegisteredServer((Context) instance);
+
+ XMPPTCPConnectionConfiguration.Builder configBuilder = XmppUtil.getConfigBuilder(instance);
+ configBuilder.setServiceName(instance.registeredXmppServer.getHostname())
+ .setPort(registeredXmppServer.getPort())
+ .setUsernameAndPassword(instance.registeredXmppServer.getUsername(), instance.registeredXmppServer.getPassword())
+ .setSendPresence(true)
+ .setDebuggerEnabled(false);
+ instance.connection = new XMPPTCPConnection(configBuilder.build());
+
+ instance.connection.setPacketReplyTimeout(XmppUtil.XMPP_STANZA_TIMEOUT);
+
+ instance.connection.setUseStreamManagement(true);
+ instance.connection.setUseStreamManagementDefault(true);
+ instance.connection.setUseStreamManagementResumption(true);
+ instance.connection.setUseStreamManagementResumptionDefault(true);
+ instance.connection.addConnectionListener(new XmppServiceConnectionListener(instance));
+
+ DeliveryReceiptManager deliveryReceiptManager = DeliveryReceiptManager.getInstanceFor(instance.connection);
+ deliveryReceiptManager.autoAddDeliveryReceiptRequests();
+ deliveryReceiptManager.addReceiptReceivedListener(new XmppServiceDeliveryReceiptListener(instance));
+ instance.connection.addPacketListener(new XmppServiceDeliveryReceiptDemandListener(instance), new PacketTypeFilter(Message.class));
+
+ reconnectionManager = ReconnectionManager.getInstanceFor(instance.connection);
+ reconnectionManager.enableAutomaticReconnection();
+ reconnectionManager.setEnabledPerDefault(true);
+ reconnectionManager.setFixedDelay(RECONNECTION_DELAY);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ AlarmManager alarmManager = ServiceUtil.getAlarmManager(this);
+ Intent intent = new Intent(this, IdleReceiver.class);
+ alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + IDLE, PendingIntent.getBroadcast(this, 0, intent, 0));
+ }
+
+ connect();
+ }
+
+ private void connect() {
+ Log.w(TAG, "Connecting...");
+ try {
+ instance.updateXmppStatus(CONNECTING);
+ connection.connect();
+ Log.w(TAG, "Connected!");
+ connection.login();
+ Log.w(TAG, "Authenticated!");
+
+ ServerPingWithAlarmManager.getInstanceFor(connection).setEnabled(true);
+
+ Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.accept_all);
+ roster = Roster.getInstanceFor(instance.connection);
+ roster.reload();
+ roster.addRosterListener(new XmppRosterListener(instance));
+ roster.setSubscriptionMode(Roster.SubscriptionMode.accept_all);
+
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(instance.connection);
+
+ List services = serviceDiscoveryManager.findServices("urn:xmpp:http:upload", true, true);
+
+ if (!services.isEmpty()) {
+ String httpUploadUrl = services.get(0);
+ if (!httpUploadUrl.isEmpty()) {
+ Log.w(TAG, "Found upload service at: " + httpUploadUrl);
+ DataForm data = DataForm.from(serviceDiscoveryManager.discoverInfo(httpUploadUrl));
+ for (FormField field : data.getFields()) {
+ if (field.getVariable().equals("max-file-size")){
+ long maxFileSize = Long.parseLong(field.getValues().get(0));
+ httpUploadService = new HttpUploadService(httpUploadUrl, maxFileSize);
+ Log.w(TAG, "max-file-size: " + maxFileSize);
+ }
+ }
+ }
+ }
+
+ Log.w(TAG, "isSmAvailable(): " + connection.isSmAvailable());
+ Log.w(TAG, "isSmEnabled(): " + connection.isSmEnabled());
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ if (!isConnected()) {
+ instance.updateXmppStatus(XmppService.DISCONNECTED);
+ instance.displayFailureNotification();
+ try {
+ Thread.sleep(RECONNECTION_DELAY*1000);
+ } catch (Exception ex) {
+ Thread.currentThread().interrupt();
+ }
+ connect();
+ }
+ }
+ }
+
+ public void reconnect() {
+ if (connection != null && !connection.isAuthenticated() && getConnectionStatus().equals(DISCONNECTED)) {
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ connect();
+ return null;
+ }
+ }.execute();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.w(TAG, "onDestroy()");
+ if (connection != null) connection.disconnect();
+ ServerPingWithAlarmManager.onDestroy();
+ instance = null;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ public static @Nullable XmppService getInstance() {
+ Log.w(TAG, "getInstance()");
+ return instance;
+ }
+
+ public Integer getConnectionStatus() {
+ return connectionStatus;
+ }
+
+ public boolean isConnected() {
+ return getConnectionStatus().equals(CONNECTED);
+ }
+
+ public void setHasBeenConnected() {
+ instance.hasBeenConnected = true;
+ }
+
+ public void setHasNotBeenConnected() {
+ instance.hasBeenConnected = false;
+ }
+
+ public boolean hasBeenConnected() {
+ Log.w(TAG, "hasBeenConnected(): " + instance.hasBeenConnected);
+ return instance.hasBeenConnected;
+ }
+
+ private void displayFailureNotification() {
+ if (!SilencePreferences.isXmppRegistered((Context) instance)) return;
+ if (!hasBeenConnected()) return;
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+
+ Intent targetIntent = getPackageManager().getLaunchIntentForPackage(getPackageName());
+ Notification notification = new NotificationCompat.Builder(this)
+ .setSmallIcon(R.drawable.ic_warning_dark)
+ .setContentTitle(getString(R.string.XmppService_xmpp_connection_failed))
+ .setContentText(getString(R.string.XmppService_xmpp_features_in_silence_are_disabled))
+ .setAutoCancel(true)
+ .setContentIntent(PendingIntent.getActivity(this, 0,
+ targetIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT))
+ .setPriority(Notification.PRIORITY_DEFAULT)
+ .build();
+
+ ServiceUtil.getNotificationManager(this).notify(SERVICE_RUNNING_ID, notification);
+ }
+
+ private boolean isXmppRecipient(Recipient recipient) {
+ return recipient.getXmppJid() != null;
+ }
+
+ public String send(String body, Recipient recipient)
+ throws UndeliverableMessageException
+ {
+ if (!isXmppRecipient(recipient)) throw new UndeliverableMessageException("JID for " + recipient.getNumber() + " is null");
+
+ ChatManager chatmanager = ChatManager.getInstanceFor(connection);
+ Chat chat = chatmanager.createChat(recipient.getXmppJid(), null);
+
+ try {
+ Message message = new Message(recipient.getXmppJid(), body);
+ String deliveryReceiptId = null;
+ if (SilencePreferences.isSmsDeliveryReportsEnabled(this)) {
+ deliveryReceiptId = DeliveryReceiptRequest.addTo(message);
+ }
+ chat.sendMessage(message);
+ return deliveryReceiptId;
+ }
+ catch (Exception e) {
+ throw new UndeliverableMessageException(e);
+ }
+ }
+
+ public void sendMedia(final MasterSecret masterSecret, String message, Attachment attachement, final long messageId, final Recipients recipients, final byte[] pduBytes)
+ throws UndeliverableMessageException
+ {
+ if (!isXmppRecipient(recipients.getPrimaryRecipient())) throw new UndeliverableMessageException("JID for " + recipients.getPrimaryRecipient().getNumber() + " is null");
+ if (httpUploadService == null) throw new UndeliverableMessageException("No upload service available");
+
+ final MediaType contentType = MediaType.parse(attachement.getContentType());
+
+ try {
+ if (contentType == null) throw new UndeliverableMessageException("Invalid Content-Type!");
+ Log.w(TAG, "length: " + pduBytes.length);
+ new XmppHttpUpload(connection).requestSlot(httpUploadService.getHostname(), messageId, pduBytes.length, attachement.getContentType(), new SlotGrantedListener() {
+ @Override
+ public void slotGranted(String put, String get){
+ sendFileAndNotify(masterSecret, messageId, pduBytes, contentType, recipients, put, get);
+ };
+
+ @Override
+ public void slotDenied(String error) {
+ Log.w(TAG, "Slot denied! " + error);
+ markFileAsErrored(messageId);
+ };
+ });
+ } catch(Exception e) {
+ throw new UndeliverableMessageException(e);
+ }
+ }
+
+ private void sendFileAndNotify(MasterSecret masterSecret, long messageId, byte[] pduBytes, MediaType contentType, Recipients recipients, String put, String get) {
+ Log.w(TAG, "sendFileAndNotify()");
+ Log.w(TAG, "Sending message ID " + messageId + " to " + put);
+
+ try {
+ MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(instance);
+ EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(instance);
+
+ OkHttpClient client = new OkHttpClient().newBuilder().connectTimeout(XMPP_SEND_MEDIA_TIMEOUT, TimeUnit.SECONDS)
+ .writeTimeout(XMPP_SEND_MEDIA_TIMEOUT, TimeUnit.SECONDS)
+ .readTimeout(XMPP_SEND_MEDIA_TIMEOUT, TimeUnit.SECONDS)
+ .build();
+
+ RequestBody body = RequestBody.create(contentType, pduBytes);
+ Request request = new Request.Builder().header("User-Agent", USER_AGENT).url(put).put(body).build();
+ Response response = client.newCall(request).execute();
+
+ if (response.code() == 200 || response.code() == 201) {
+ OutgoingTextMessage message = new SmsCipher(new SilenceAxolotlStore(instance, masterSecret)).encrypt(new OutgoingEncryptedMessage(recipients, get, -1));
+
+ MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
+ ArrayList messages = multipartMessageHandler.divideMessage(message, true);
+
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String string : messages) {
+ stringBuilder.append(string.trim());
+ }
+
+ String deliveryReceiptId = send(stringBuilder.toString(), recipients.getPrimaryRecipient());
+ Log.w(TAG, "deliveryReceiptId: " + deliveryReceiptId);
+
+ smsDatabase.updateXmppId(messageId, deliveryReceiptId);
+ mmsDatabase.markAsSent(messageId);
+ } else {
+ Log.w(TAG, "Received " + response.code() + " code...");
+ markFileAsErrored(messageId);
+ }
+
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ markFileAsErrored(messageId);
+ }
+
+
+ }
+
+ private void markFileAsErrored(long messageId) {
+ Log.w(TAG, "markFileAsErrored()");
+ MmsDatabase database = DatabaseFactory.getMmsDatabase(instance);
+ database.markAsSentFailed(messageId);
+ MmsSendJob.notifyMediaMessageDeliveryFailed(instance, messageId);
+ }
+
+ private void updateXmppStatus(Integer status) {
+ Integer oldStatus = getConnectionStatus();
+ connectionStatus = status;
+ if (hasBeenConnected()) broadcastXmppConnectivityEvent(this);
+ }
+
+ private static void broadcastXmppConnectivityEvent(Context context) {
+ Intent intent = new Intent(XMPP_CONNECTIVITY_EVENT);
+ intent.setPackage(context.getPackageName());
+ context.sendBroadcast(intent);
+ }
+
+ public void subscribe(String jid) {
+ Presence subscribe = new Presence(Presence.Type.subscribe);
+ subscribe.setTo(jid);
+ try {
+ connection.sendPacket(subscribe);
+ } catch (Exception e) {
+ Log.w(TAG, "Impossible NotConnectedException?");
+ }
+ }
+
+ public void deleteAccount() {
+
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (connection != null) {
+ reconnectionManager.disableAutomaticReconnection();
+ AccountManager accountManager = AccountManager.getInstance(connection);
+ try {
+ accountManager.deleteAccount();
+ } catch (Exception e) {}
+ for (Recipient xmppRecipient : RecipientFactory.getXmppRecipients(instance, false)){
+ xmppRecipient.setXmppJid(null);
+ }
+ SilencePreferences.disableXmpp((Context) instance);
+ SilencePreferences.setXmppUsername((Context) instance, "");
+ SilencePreferences.setXmppPassword((Context) instance, "");
+ SilencePreferences.setXmppHostname((Context) instance, "");
+ SilencePreferences.setXmppPort((Context) instance, 0);
+ XmppUtil.stopService((Context) instance);
+ } else {
+ Log.w(TAG, "connection is null, cannot delete account");
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+ public Presence getRecipientPresence(String jid) {
+ if (roster == null) {
+ Log.w(TAG, "Returning null Presence for " + jid);
+ return null;
+ }
+ return roster.getPresence(jid);
+ }
+
+ private class XmppServiceConnectionListener implements ConnectionListener {
+ private Context context;
+ private XmppService instance;
+
+ public XmppServiceConnectionListener(XmppService instance) {
+ this.context = (Context) instance;
+ this.instance = instance;
+ }
+
+ @Override
+ public void authenticated(XMPPConnection connection, boolean resumed) {
+ instance.setHasBeenConnected();
+ instance.updateXmppStatus(XmppService.CONNECTED);
+ ServiceUtil.getNotificationManager(context).cancel(XmppService.SERVICE_RUNNING_ID);
+ }
+
+ @Override
+ public void connected(XMPPConnection connection) {
+ ChatManager chatManager = ChatManager.getInstanceFor(connection);
+ if (chatManager.getChatListeners().size() < 1) {
+ chatManager.addChatListener(new ChatManagerListener() {
+ @Override
+ public void chatCreated(Chat chat, boolean createdLocally) {
+ if (!createdLocally) chat.addMessageListener(new XmppServiceChatMessageListener(context));
+ }
+ });
+ }
+ }
+
+ @Override
+ public void connectionClosed() {
+ Log.w(TAG, "connectionClosed()");
+
+ if (!SilencePreferences.isXmppRegistered(getApplicationContext())) {
+ Log.w(TAG, "Xmpp is disabled.");
+ } else {
+ instance.updateXmppStatus(XmppService.DISCONNECTED);
+ instance.displayFailureNotification();
+ instance.setHasNotBeenConnected();
+ connect();
+ }
+ }
+
+ @Override
+ public void connectionClosedOnError(Exception e) {
+ Log.w(TAG, "connectionClosedOnError()");
+ Log.w(TAG, e);
+
+ if (!SilencePreferences.isXmppRegistered(getApplicationContext())) {
+ Log.w(TAG, "Xmpp is disabled.");
+ } else {
+ instance.updateXmppStatus(XmppService.DISCONNECTED);
+ instance.displayFailureNotification();
+ instance.setHasNotBeenConnected();
+ connect();
+ }
+ }
+
+ @Override
+ public void reconnectingIn(int seconds) {
+ Log.w(TAG, "reconnectingIn(" + seconds + ")");
+ }
+
+ @Override
+ public void reconnectionFailed(Exception e) {
+ Log.w(TAG, "reconnectionFailed()");
+ instance.updateXmppStatus(XmppService.DISCONNECTED);
+ }
+
+ @Override
+ public void reconnectionSuccessful() {
+ Log.w(TAG, "reconnectionSuccessful()");
+ instance.updateXmppStatus(XmppService.CONNECTED);
+ ServiceUtil.getNotificationManager(context).cancel(XmppService.SERVICE_RUNNING_ID);
+ }
+ }
+
+ private class XmppServiceChatMessageListener implements ChatMessageListener {
+ private final Context context;
+
+ public XmppServiceChatMessageListener (Context context) {
+ Log.w(TAG, "Starting XMPP messages receiver...");
+ this.context = context;
+ }
+
+ @Override
+ public void processMessage(Chat chat, Message message){
+ String senderJid = chat.getParticipant().replaceAll("\\/.*$","");
+
+ Log.w(TAG, "Message recieved from " + senderJid);
+ ArrayList xmpp = new ArrayList();
+ xmpp.add(senderJid);
+
+ Recipient sender = RecipientFactory.getRecipientsFromXmpp(context, xmpp, false).getPrimaryRecipient();
+ if (sender != null) {
+ for (Message.Body body : message.getBodies()) {
+ if (body.getMessage() != null) ApplicationContext.getInstance(context).getJobManager().add(new SmsReceiveJob(context, body.getMessage().trim(), sender.getNumber(), -1));
+ }
+ }
+ }
+ }
+
+ private class XmppServiceDeliveryReceiptDemandListener implements PacketListener {
+ XmppService instance;
+
+ public XmppServiceDeliveryReceiptDemandListener(XmppService instance) {
+ this.instance = instance;
+ }
+
+ @Override
+ public void processPacket(Stanza packet) {
+ if (packet instanceof Message && packet.getExtension("request", DeliveryReceipt.NAMESPACE) != null) {
+ try {
+ Message response = new Message(packet.getFrom().replaceAll("\\/.*$",""), Message.Type.normal);
+ response.addExtension(new DeliveryReceipt(packet.getPacketID()));
+ instance.connection.sendPacket(response);
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ }
+ }
+ }
+ }
+
+ private class XmppServiceDeliveryReceiptListener implements ReceiptReceivedListener {
+ XmppService instance;
+
+ public XmppServiceDeliveryReceiptListener (XmppService instance) {
+ this.instance = instance;
+ }
+
+ @Override
+ public void onReceiptReceived(String fromJid, String toJid, String deliveryReceiptId, Stanza stanza) {
+ Log.d(TAG, "onReceiptReceived: from: " + fromJid + " to: " + toJid + " deliveryReceiptId: " + deliveryReceiptId + " stanza: " + stanza);
+ EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(instance);
+ long messageId = database.getMessageIdFromXmpp(deliveryReceiptId);
+ if (messageId > -1) {
+ ApplicationContext.getInstance(instance).getJobManager()
+ .add(new SmsSentJob(instance, messageId, SmsDeliveryListener.DELIVERED_SMS_ACTION, Activity.RESULT_OK));
+
+ } else {
+ Log.w(TAG, "Received delivery receipt for an unknown message");
+ }
+ }
+ }
+
+ private class XmppRosterListener implements RosterListener {
+ private XmppService instance;
+
+ public XmppRosterListener (XmppService instance) {
+ this.instance = instance;
+ }
+
+ @Override
+ public void entriesAdded(Collection addresses) {}
+
+ @Override
+ public void entriesDeleted(Collection addresses) {}
+
+ @Override
+ public void entriesUpdated(Collection addresses) {}
+
+ @Override
+ public void presenceChanged(Presence presence) {
+ instance.broadcastXmppConnectivityEvent((Context) instance);
+ }
+ }
+
+ public class IdleReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ }
+ }
+
+ private class HttpUploadService {
+ private String hostname;
+ private long maxFileSize;
+
+ public HttpUploadService(String hostname, long maxFileSize) {
+ this.hostname = hostname;
+ this.maxFileSize = maxFileSize;
+ }
+
+ public String getHostname() {
+ return hostname;
+ }
+
+ public long getMaxFileSize() {
+ return maxFileSize;
+ }
+ }
+}
diff --git a/src/org/smssecure/smssecure/service/XmppServiceBoot.java b/src/org/smssecure/smssecure/service/XmppServiceBoot.java
new file mode 100644
index 0000000000000000000000000000000000000000..fabb93b1746c3d889d329cb449fdf6b9004e8052
--- /dev/null
+++ b/src/org/smssecure/smssecure/service/XmppServiceBoot.java
@@ -0,0 +1,28 @@
+package org.smssecure.smssecure.service;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import org.smssecure.smssecure.util.XmppUtil;
+
+public class XmppServiceBoot extends BroadcastReceiver {
+ private static final String TAG = XmppServiceBoot.class.getSimpleName();
+
+ private static final String BOOT_COMPLETED_EVENT = "android.intent.action.BOOT_COMPLETED";
+
+ private Context context;
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ Log.w(TAG, "onReceive()");
+ this.context = context;
+
+ if (intent.getAction().equals(BOOT_COMPLETED_EVENT)) {
+ XmppUtil.startService(context);
+ } else {
+ throw new AssertionError("Unknown event passed to XmppServiceBoot");
+ }
+ }
+}
diff --git a/src/org/smssecure/smssecure/sms/IncomingTextMessage.java b/src/org/smssecure/smssecure/sms/IncomingTextMessage.java
index 9e961ce872703e2c371dc1631e7d92954a5fd5ba..b1c90d3ae2c5d9a582dba973661dfe2500a58698 100644
--- a/src/org/smssecure/smssecure/sms/IncomingTextMessage.java
+++ b/src/org/smssecure/smssecure/sms/IncomingTextMessage.java
@@ -32,7 +32,7 @@ public class IncomingTextMessage implements Parcelable {
private final String pseudoSubject;
private final long sentTimestampMillis;
private final String groupId;
- private final boolean push;
+ private final boolean xmpp;
private final int subscriptionId;
private final boolean receivedWhenLocked;
@@ -51,11 +51,15 @@ public class IncomingTextMessage implements Parcelable {
this.sentTimestampMillis = message.getTimestampMillis();
this.subscriptionId = subscriptionId;
this.groupId = null;
- this.push = false;
+ this.xmpp = false;
this.receivedWhenLocked = receivedWhenLocked;
}
public IncomingTextMessage(String sender, int senderDeviceId, long sentTimestampMillis, String encodedBody) {
+ this(sender, senderDeviceId, sentTimestampMillis, encodedBody, false);
+ }
+
+ public IncomingTextMessage(String sender, int senderDeviceId, long sentTimestampMillis, String encodedBody, boolean receivedWhenLocked) {
this.message = encodedBody;
this.sender = sender;
this.senderDeviceId = senderDeviceId;
@@ -64,10 +68,10 @@ public class IncomingTextMessage implements Parcelable {
this.replyPathPresent = true;
this.pseudoSubject = "";
this.sentTimestampMillis = sentTimestampMillis;
- this.push = true;
+ this.xmpp = true;
this.subscriptionId = -1;
- this.groupId = null;
- this.receivedWhenLocked = false;
+ this.groupId = null;
+ this.receivedWhenLocked = receivedWhenLocked;
}
public IncomingTextMessage(Parcel in) {
@@ -80,7 +84,7 @@ public class IncomingTextMessage implements Parcelable {
this.pseudoSubject = in.readString();
this.sentTimestampMillis = in.readLong();
this.groupId = in.readString();
- this.push = (in.readInt() == 1);
+ this.xmpp = (in.readInt() == 1);
this.subscriptionId = in.readInt();
this.receivedWhenLocked = (in.readInt() == 1);
}
@@ -95,7 +99,7 @@ public class IncomingTextMessage implements Parcelable {
this.pseudoSubject = base.getPseudoSubject();
this.sentTimestampMillis = base.getSentTimestampMillis();
this.groupId = base.getGroupId();
- this.push = base.isPush();
+ this.xmpp = base.isXmpp();
this.subscriptionId = base.getSubscriptionId();
this.receivedWhenLocked = base.isReceivedWhenLocked();
}
@@ -116,7 +120,7 @@ public class IncomingTextMessage implements Parcelable {
this.pseudoSubject = fragments.get(0).getPseudoSubject();
this.sentTimestampMillis = fragments.get(0).getSentTimestampMillis();
this.groupId = fragments.get(0).getGroupId();
- this.push = fragments.get(0).isPush();
+ this.xmpp = fragments.get(0).isXmpp();
this.subscriptionId = fragments.get(0).getSubscriptionId();
this.receivedWhenLocked = fragments.get(0).isReceivedWhenLocked();
}
@@ -132,7 +136,7 @@ public class IncomingTextMessage implements Parcelable {
this.pseudoSubject = "";
this.sentTimestampMillis = System.currentTimeMillis();
this.groupId = groupId;
- this.push = true;
+ this.xmpp = true;
this.subscriptionId = -1;
this.receivedWhenLocked = false;
}
@@ -197,8 +201,8 @@ public class IncomingTextMessage implements Parcelable {
return false;
}
- public boolean isPush() {
- return push;
+ public boolean isXmpp() {
+ return xmpp;
}
public String getGroupId() {
@@ -229,7 +233,7 @@ public class IncomingTextMessage implements Parcelable {
out.writeString(pseudoSubject);
out.writeLong(sentTimestampMillis);
out.writeString(groupId);
- out.writeInt(push ? 1 : 0);
+ out.writeInt(xmpp ? 1 : 0);
out.writeInt(subscriptionId);
out.writeInt(receivedWhenLocked ? 1 : 0);
}
diff --git a/src/org/smssecure/smssecure/sms/MessageSender.java b/src/org/smssecure/smssecure/sms/MessageSender.java
index f230d4221eff5b38a755080d549591c78a44394e..d91ece97fefdc23b453e601337bfcc072f0967a3 100644
--- a/src/org/smssecure/smssecure/sms/MessageSender.java
+++ b/src/org/smssecure/smssecure/sms/MessageSender.java
@@ -37,6 +37,7 @@ import org.smssecure.smssecure.util.GroupUtil;
import org.smssecure.smssecure.util.InvalidNumberException;
import org.smssecure.smssecure.util.SilencePreferences;
import org.smssecure.smssecure.util.Util;
+import org.smssecure.smssecure.util.XmppUtil;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.libaxolotl.util.guava.Optional;
@@ -48,15 +49,25 @@ public class MessageSender {
private static final String TAG = MessageSender.class.getSimpleName();
+ public static long send(final Context context,
+ final MasterSecret masterSecret,
+ final OutgoingTextMessage message,
+ final long threadId)
+ {
+ Recipient primaryRecipient = message.getRecipients() == null ? null : message.getRecipients().getPrimaryRecipient();
+ boolean isXmppAvailable = XmppUtil.isXmppAvailable(context) && primaryRecipient.getXmppJid() != null;
+
+ return send(context, masterSecret, message, threadId, isXmppAvailable);
+ }
+
public static long send(final Context context,
final MasterSecret masterSecret,
final OutgoingTextMessage message,
final long threadId,
- final boolean forceSms)
+ final boolean isXmpp)
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Recipients recipients = message.getRecipients();
- boolean keyExchange = message.isKeyExchange();
long allocatedThreadId;
@@ -66,7 +77,8 @@ public class MessageSender {
allocatedThreadId = threadId;
}
- long messageId = database.insertMessageOutbox(masterSecret, allocatedThreadId, message, forceSms, System.currentTimeMillis());
+ long messageId = database.insertMessageOutbox(masterSecret, allocatedThreadId, message, System.currentTimeMillis());
+ if (isXmpp && !message.isXmppExchange()) database.markAsXmpp(messageId);
sendTextMessage(context, recipients, messageId);
@@ -77,7 +89,7 @@ public class MessageSender {
final MasterSecret masterSecret,
final OutgoingMediaMessage message,
final long threadId,
- final boolean forceSms)
+ final boolean isXmpp)
{
try {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
@@ -91,8 +103,8 @@ public class MessageSender {
allocatedThreadId = threadId;
}
- Recipients recipients = message.getRecipients();
- long messageId = database.insertMessageOutbox(masterSecret, message, allocatedThreadId, forceSms);
+ long messageId = database.insertMessageOutbox(masterSecret, message, allocatedThreadId);
+ if (isXmpp) database.markAsXmpp(messageId);
sendMediaMessage(context, messageId);
diff --git a/src/org/smssecure/smssecure/sms/MultipartSmsMessageHandler.java b/src/org/smssecure/smssecure/sms/MultipartSmsMessageHandler.java
index c944dae16abeb7e410b86433265ad6ad8ed04855..2e88a0ec5d8b8eda155390d83111eb1917535ae3 100644
--- a/src/org/smssecure/smssecure/sms/MultipartSmsMessageHandler.java
+++ b/src/org/smssecure/smssecure/sms/MultipartSmsMessageHandler.java
@@ -94,9 +94,9 @@ public class MultipartSmsMessageHandler {
}
}
- public synchronized ArrayList divideMessage(OutgoingTextMessage message) {
+ public synchronized ArrayList divideMessage(OutgoingTextMessage message, boolean isXmpp) {
String number = message.getRecipients().getPrimaryRecipient().getNumber();
byte identifier = MultipartSmsIdentifier.getInstance().getIdForRecipient(number);
- return MultipartSmsTransportMessage.getEncoded(message, identifier);
+ return MultipartSmsTransportMessage.getEncoded(message, identifier, isXmpp);
}
}
diff --git a/src/org/smssecure/smssecure/sms/MultipartSmsTransportMessage.java b/src/org/smssecure/smssecure/sms/MultipartSmsTransportMessage.java
index d9dcf9e42bb640496dd72ed00528b54b090e88b9..170539ab9b63a0fe4b481fca6dac47cab74d7463 100644
--- a/src/org/smssecure/smssecure/sms/MultipartSmsTransportMessage.java
+++ b/src/org/smssecure/smssecure/sms/MultipartSmsTransportMessage.java
@@ -154,7 +154,7 @@ public class MultipartSmsTransportMessage {
return message;
}
- public static ArrayList getEncoded(OutgoingTextMessage message, byte identifier)
+ public static ArrayList getEncoded(OutgoingTextMessage message, byte identifier, boolean isXmpp)
{
try {
byte[] decoded = Base64.decodeWithoutPadding(message.getMessageBody());
@@ -165,10 +165,11 @@ public class MultipartSmsTransportMessage {
if (message.isKeyExchange()) prefix = new KeyExchangeWirePrefix();
else if (message.isPreKeyBundle()) prefix = new PrekeyBundleWirePrefix();
else if (message.isEndSession()) prefix = new EndSessionWirePrefix();
+ else if (message.isXmppExchange()) prefix = new XmppExchangeWirePrefix();
else prefix = new SecureMessageWirePrefix();
- if (count == 1) return getSingleEncoded(decoded, prefix);
- else return getMultiEncoded(decoded, prefix, count, identifier);
+ if (count == 1 || isXmpp) return getSingleEncoded(decoded, prefix);
+ else return getMultiEncoded(decoded, prefix, count, identifier);
} catch (IOException e) {
throw new AssertionError(e);
diff --git a/src/org/smssecure/smssecure/sms/OutgoingTextMessage.java b/src/org/smssecure/smssecure/sms/OutgoingTextMessage.java
index 0257d1c59ec346495af6a0246aa34dd1160f6ebf..7e6d70b73f50418659e683f73a10ba803f29e060 100644
--- a/src/org/smssecure/smssecure/sms/OutgoingTextMessage.java
+++ b/src/org/smssecure/smssecure/sms/OutgoingTextMessage.java
@@ -49,9 +49,15 @@ public class OutgoingTextMessage {
return false;
}
+ public boolean isXmppExchange() {
+ return false;
+ }
+
public static OutgoingTextMessage from(SmsMessageRecord record) {
if (record.isSecure()) {
return new OutgoingEncryptedMessage(record.getRecipients(), record.getBody().getBody(), record.getSubscriptionId());
+ } else if (record.isXmppExchange()) {
+ return new OutgoingXmppExchangeMessage(record.getRecipients(), record.getBody().getBody(), record.getSubscriptionId());
} else if (record.isKeyExchange()) {
return new OutgoingKeyExchangeMessage(record.getRecipients(), record.getBody().getBody(), record.getSubscriptionId());
} else if (record.isEndSession()) {
diff --git a/src/org/smssecure/smssecure/sms/OutgoingXmppExchangeMessage.java b/src/org/smssecure/smssecure/sms/OutgoingXmppExchangeMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..c6305fed68762731e6abe0a60ed0a44d0f83ffb7
--- /dev/null
+++ b/src/org/smssecure/smssecure/sms/OutgoingXmppExchangeMessage.java
@@ -0,0 +1,28 @@
+package org.smssecure.smssecure.sms;
+
+import org.smssecure.smssecure.recipients.Recipients;
+
+public class OutgoingXmppExchangeMessage extends OutgoingTextMessage {
+
+ public OutgoingXmppExchangeMessage(OutgoingTextMessage base) {
+ this(base, base.getMessageBody());
+ }
+
+ public OutgoingXmppExchangeMessage(OutgoingTextMessage message, String body) {
+ super(message, body);
+ }
+
+ public OutgoingXmppExchangeMessage(Recipients recipients, String message, int subscriptionId) {
+ super(recipients, message, subscriptionId);
+ }
+
+ @Override
+ public boolean isXmppExchange() {
+ return true;
+ }
+
+ @Override
+ public OutgoingTextMessage withBody(String body) {
+ return new OutgoingXmppExchangeMessage(this, body);
+ }
+}
diff --git a/src/org/smssecure/smssecure/util/ServiceUtil.java b/src/org/smssecure/smssecure/util/ServiceUtil.java
index 8c7ce73c3c895638aed4abc9d29b6bec03829eee..b99f77d7a0a4ad8dca93acdf5fef5b3fb01848c3 100644
--- a/src/org/smssecure/smssecure/util/ServiceUtil.java
+++ b/src/org/smssecure/smssecure/util/ServiceUtil.java
@@ -1,7 +1,9 @@
package org.smssecure.smssecure.util;
import android.app.Activity;
+import android.app.AlarmManager;
import android.app.NotificationManager;
+import android.content.ClipboardManager;
import android.content.Context;
import android.media.AudioManager;
import android.net.ConnectivityManager;
@@ -33,4 +35,12 @@ public class ServiceUtil {
public static AudioManager getAudioManager(Context context) {
return (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
}
+
+ public static ClipboardManager getClipboardManager(Context context) {
+ return (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ }
+
+ public static AlarmManager getAlarmManager(Context context) {
+ return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ }
}
diff --git a/src/org/smssecure/smssecure/util/SilencePreferences.java b/src/org/smssecure/smssecure/util/SilencePreferences.java
index a251a8c6d4666f7c6f74fc144ef9619a5e219c60..fdf6aeaddfbb8b6b7e8ee831c9e6e3a434b4389c 100644
--- a/src/org/smssecure/smssecure/util/SilencePreferences.java
+++ b/src/org/smssecure/smssecure/util/SilencePreferences.java
@@ -95,6 +95,15 @@ public class SilencePreferences {
private static final String MEDIA_DOWNLOAD_PREF = "pref_media_download";
private static final String MEDIA_DOWNLOAD_ROAMING_PREF = "pref_media_download_roaming";
+ public static final String XMPP_STATUS = "pref_xmpp_status";
+ public static final String XMPP_ENABLED_PREF = "pref_xmpp_enabled";
+ private static final String XMPP_USERNAME_PREF = "pref_xmpp_username";
+ private static final String XMPP_PASSWORD_PREF = "pref_xmpp_password";
+ private static final String XMPP_HOSTNAME_PREF = "pref_xmpp_hostname";
+ private static final String XMPP_PORT_PREF = "pref_xmpp_port";
+ public static final String XMPP_NOTIFY_CONTACTS = "pref_xmpp_notify";
+ private static final String XMPP_FORCE = "pref_xmpp_force";
+
public static NotificationPrivacyPreference getNotificationPrivacy(Context context) {
return new NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all"));
}
@@ -542,6 +551,67 @@ public class SilencePreferences {
return getBooleanPreference(context, SHOW_SENT_TIME, false);
}
+ public static boolean isXmppRegistered(Context context) {
+ return isXmppEnabled(context) &&
+ getXmppUsername(context) != "" &&
+ getXmppPassword(context) != "" &&
+ getXmppHostname(context) != "" &&
+ getXmppPort(context) != 0;
+ }
+
+ public static boolean isXmppEnabled(Context context) {
+ return getBooleanPreference(context, XMPP_ENABLED_PREF, false);
+ }
+
+ public static void enableXmpp(Context context) {
+ setBooleanPreference(context, XMPP_ENABLED_PREF, true);
+ }
+
+ public static void disableXmpp(Context context) {
+ setBooleanPreference(context, XMPP_ENABLED_PREF, false);
+ }
+
+ public static String getXmppJid(Context context) {
+ String xmppJid = getXmppUsername(context) + "@" + getXmppHostname(context);
+ return xmppJid.equals("@") ? "" : xmppJid;
+ }
+
+ public static String getXmppUsername(Context context) {
+ return getStringPreference(context, XMPP_USERNAME_PREF, "");
+ }
+
+ public static String getXmppPassword(Context context) {
+ return getStringPreference(context, XMPP_PASSWORD_PREF, "");
+ }
+
+ public static String getXmppHostname(Context context) {
+ return getStringPreference(context, XMPP_HOSTNAME_PREF, "");
+ }
+
+ public static int getXmppPort(Context context) {
+ return getIntegerPreference(context, XMPP_PORT_PREF, 0);
+ }
+
+ public static void setXmppUsername(Context context, String username) {
+ setStringPreference(context, XMPP_USERNAME_PREF, username);
+ }
+
+ public static void setXmppPassword(Context context, String password) {
+ setStringPreference(context, XMPP_PASSWORD_PREF, password);
+ }
+
+ public static void setXmppHostname(Context context, String hostname) {
+ setStringPreference(context, XMPP_HOSTNAME_PREF, hostname);
+ }
+
+ public static void setXmppPort(Context context, int port) {
+ setIntegerPrefrence(context, XMPP_PORT_PREF, port);
+ }
+
+ public static boolean isXmppForced(Context context) {
+ return getBooleanPreference(context, XMPP_FORCE, false);
+ }
+
public static void setBooleanPreference(Context context, String key, boolean value) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply();
}
diff --git a/src/org/smssecure/smssecure/util/XmppCharacterCalculator.java b/src/org/smssecure/smssecure/util/XmppCharacterCalculator.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5c6628cb587f221885de11677184c2c8bae1165
--- /dev/null
+++ b/src/org/smssecure/smssecure/util/XmppCharacterCalculator.java
@@ -0,0 +1,9 @@
+package org.smssecure.smssecure.util;
+
+public class XmppCharacterCalculator extends CharacterCalculator {
+ private static final int MAX_SIZE = 2000;
+ @Override
+ public CharacterState calculateCharacters(String messageBody) {
+ return new CharacterState(1, MAX_SIZE - messageBody.length(), MAX_SIZE);
+ }
+}
diff --git a/src/org/smssecure/smssecure/util/XmppUtil.java b/src/org/smssecure/smssecure/util/XmppUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..36130de32f777afddbc5e0ed121494d6b4868d6a
--- /dev/null
+++ b/src/org/smssecure/smssecure/util/XmppUtil.java
@@ -0,0 +1,240 @@
+package org.smssecure.smssecure.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import org.smssecure.smssecure.crypto.MasterSecret;
+import org.smssecure.smssecure.R;
+import org.smssecure.smssecure.recipients.RecipientFactory;
+import org.smssecure.smssecure.recipients.Recipient;
+import org.smssecure.smssecure.recipients.Recipients;
+import org.smssecure.smssecure.service.KeyCachingService;
+import org.smssecure.smssecure.service.XmppService;
+import org.smssecure.smssecure.sms.MessageSender;
+import org.smssecure.smssecure.sms.OutgoingTextMessage;
+import org.smssecure.smssecure.sms.OutgoingXmppExchangeMessage;
+import org.smssecure.smssecure.util.Base64;
+import org.smssecure.smssecure.util.dualsim.SubscriptionManagerCompat;
+import org.smssecure.smssecure.util.SilencePreferences;
+
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.tcp.XMPPTCPConnection;
+import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.iqregister.AccountManager;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.LinkedList;
+import java.util.Random;
+import java.util.UUID;
+
+public class XmppUtil {
+ private static final String TAG = XmppUtil.class.getSimpleName();
+
+ public static Integer XMPP_TIMEOUT = 10000; // 10s
+ public static Integer XMPP_STANZA_TIMEOUT = 30000; // 30s
+ private static ArrayList xmppServers = new ArrayList() {
+ {
+ add(new XmppServer("xmpp.ams1.silence.im"));
+ }
+ };
+
+ public static void tryToRegister(Context context, ArrayList serversToTest)
+ throws Exception
+ {
+ if (!SilencePreferences.isXmppRegistered(context)) {
+ if (serversToTest.size() <= 0) throw new Exception("No more XMPP server available");
+
+ Random random = new Random();
+ Integer randomInt = random.nextInt(serversToTest.size());
+ XmppServer xmppServerToRegister = serversToTest.get(randomInt);
+ Log.w(TAG, "Registering on server " + xmppServerToRegister.getHostname());
+ try {
+ register(context, xmppServerToRegister);
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ serversToTest.remove(xmppServerToRegister);
+ tryToRegister(context, serversToTest);
+ }
+ SilencePreferences.enableXmpp(context);
+ startService(context);
+ }
+ }
+
+ public static void tryToRegister(Context context)
+ throws Exception
+ {
+ tryToRegister(context, new ArrayList(getXmppServers()));
+ }
+
+ public static void register(Context context, XmppServer xmppServerToRegister)
+ throws IOException, SmackException, SmackException.NoResponseException, XMPPException, XMPPException.XMPPErrorException
+ {
+ if (!SilencePreferences.isXmppRegistered(context)) {
+ stopService(context);
+
+ String uuid = UUID.randomUUID().toString();
+ String password;
+ try {
+ SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
+ byte[] salt = new byte[64];
+ secureRandom.nextBytes(salt);
+ password = Base64.encodeBytes(salt).replaceAll("[=*$]","");
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ throw new AssertionError(e);
+ }
+
+ Log.w(TAG, "Registering with UUID " + uuid);
+
+ XMPPTCPConnectionConfiguration.Builder configBuilder = getConfigBuilder(context);
+ configBuilder.setServiceName(xmppServerToRegister.getHostname())
+ .setPort(xmppServerToRegister.getPort());
+ XMPPTCPConnection connection = new XMPPTCPConnection(configBuilder.build());
+ connection.setPacketReplyTimeout(XMPP_STANZA_TIMEOUT);
+ connection.connect();
+
+ AccountManager accountManager = AccountManager.getInstance(connection);
+ if (!accountManager.supportsAccountCreation()) {
+ throw new UnsupportedOperationException("Account registation is not supported");
+ }
+ if (accountManager.getAccountAttributes().size() > 2) {
+ throw new UnsupportedOperationException("Anonymous account registation is not supported");
+ }
+ accountManager.createAccount(uuid, password);
+
+ SilencePreferences.setXmppUsername(context, uuid);
+ SilencePreferences.setXmppPassword(context, password);
+ SilencePreferences.setXmppHostname(context, xmppServerToRegister.getHostname());
+ SilencePreferences.setXmppPort(context, xmppServerToRegister.getPort());
+
+ connection.disconnect();
+ }
+ }
+
+ public static XMPPTCPConnectionConfiguration.Builder getConfigBuilder(Context context) {
+ XMPPTCPConnectionConfiguration.Builder configBuilder = XMPPTCPConnectionConfiguration.builder();
+ configBuilder.setResource(context.getString(R.string.app_name))
+ .setSecurityMode(ConnectionConfiguration.SecurityMode.required)
+ .setEnabledSSLProtocols(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" })
+ .setConnectTimeout(XMPP_TIMEOUT)
+ .setDebuggerEnabled(false);
+ return configBuilder;
+ }
+
+ public static void startService(Context context) {
+ Log.w(TAG, "startService()");
+
+ if (XmppService.getInstance() == null) {
+ Log.w(TAG, "Starting service...");
+ context.startService(new Intent(context, XmppService.class));
+ }
+ }
+
+ public static void stopService(Context context) {
+ context.stopService(new Intent(context, XmppService.class));
+ }
+
+ public static @NonNull ArrayList getXmppServers() {
+ return xmppServers;
+ }
+
+ public static @Nullable XmppServer getRegisteredServer(Context context) {
+ if (SilencePreferences.isXmppRegistered(context)) {
+ return new XmppServer(SilencePreferences.getXmppUsername(context),
+ SilencePreferences.getXmppPassword(context),
+ SilencePreferences.getXmppHostname(context),
+ SilencePreferences.getXmppPort(context));
+ } else {
+ return null;
+ }
+ }
+
+ public static boolean isXmppAvailable(Context context) {
+ XmppService instance = XmppService.getInstance();
+ return SilencePreferences.isXmppRegistered(context) &&
+ instance != null &&
+ instance.isConnected();
+ }
+
+ public static void sendNullXmppMessage(final Context context) {
+ MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
+ if (masterSecret == null) throw new AssertionError("null masterSecret");
+
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(MasterSecret... params) {
+ for (Recipient recipient : RecipientFactory.getXmppRecipients(context, false)) {
+ Recipients recipients = RecipientFactory.getRecipientsFor(context, recipient, false);
+ OutgoingXmppExchangeMessage xmppExchangeMessage =
+ new OutgoingXmppExchangeMessage(recipients, "NULL", SubscriptionManagerCompat.getDefaultMessagingSubscriptionId().or(-1));
+ MessageSender.send(context, params[0], xmppExchangeMessage, -1, false);
+ }
+
+ return null;
+ }
+ }.execute(masterSecret);
+ }
+
+ public static class XmppServer {
+ private String username;
+ private String password;
+ private String hostname;
+ private Integer port;
+
+ public XmppServer(String username, String password, String hostname, Integer port){
+ this.username = username;
+ this.password = password;
+ this.hostname = hostname;
+ this.port = port;
+ }
+
+ public XmppServer(String username, String password, String hostname){
+ this.username = username;
+ this.password = password;
+ this.hostname = hostname;
+ this.port = 5222;
+ }
+
+ public XmppServer(String hostname, Integer port) {
+ this.username = null;
+ this.password = null;
+ this.hostname = hostname;
+ this.port = port;
+ }
+
+ public XmppServer(String hostname) {
+ this.hostname = hostname;
+ this.port = 5222;
+ }
+
+ public String getUsername() {
+ return this.username;
+ }
+
+ public String getPassword() {
+ return this.password;
+ }
+
+ public String getHostname() {
+ return this.hostname;
+ }
+
+ public Integer getPort() {
+ return this.port;
+ }
+
+ public String toString() {
+ return this.hostname + ":" + this.port;
+ }
+ }
+
+}