diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d415492c83..33be2302ff 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -34,7 +34,7 @@
-
diff --git a/res/layout/registration_progress_activity.xml b/res/layout/registration_progress_activity.xml
index 3cf4797711..4032aa0a5b 100644
--- a/res/layout/registration_progress_activity.xml
+++ b/res/layout/registration_progress_activity.xml
@@ -456,6 +456,48 @@
android:textSize="16.0sp" />
+
+
+
+
+
+
+
+
+
+
+
recipientList = new ArrayList(1);
- recipientList.add(new Recipient(null, message.getDisplayOriginatingAddress(), null, null));
+ recipientList.add(new Recipient(null, message.getSender(), null, null));
Recipients recipients = new Recipients(recipientList);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
ContentValues values = new ContentValues(6);
- values.put(ADDRESS, message.getDisplayOriginatingAddress());
+ values.put(ADDRESS, message.getSender());
values.put(DATE_RECEIVED, Long.valueOf(System.currentTimeMillis()));
values.put(DATE_SENT, timeSent);
- values.put(PROTOCOL, message.getProtocolIdentifier());
+ values.put(PROTOCOL, message.getProtocol());
values.put(READ, Integer.valueOf(0));
if (message.getPseudoSubject().length() > 0)
@@ -212,13 +212,13 @@ public class SmsDatabase extends Database {
notifyConversationListListeners();
}
- public long insertSecureMessageReceived(SmsMessage message, String body) {
+ public long insertSecureMessageReceived(TextMessage message, String body) {
return insertMessageReceived(message, body, Types.DECRYPT_IN_PROGRESS_TYPE,
- message.getTimestampMillis());
+ message.getSentTimestampMillis());
}
- public long insertMessageReceived(SmsMessage message, String body) {
- return insertMessageReceived(message, body, Types.INBOX_TYPE, message.getTimestampMillis());
+ public long insertMessageReceived(TextMessage message, String body) {
+ return insertMessageReceived(message, body, Types.INBOX_TYPE, message.getSentTimestampMillis());
}
public long insertMessageSent(String address, long threadId, String body, long date, long type) {
diff --git a/src/org/thoughtcrime/securesms/database/InvalidKeyIdException.java b/src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java
similarity index 96%
rename from src/org/thoughtcrime/securesms/database/InvalidKeyIdException.java
rename to src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java
index aa34024b90..c46a8897b9 100644
--- a/src/org/thoughtcrime/securesms/database/InvalidKeyIdException.java
+++ b/src/org/thoughtcrime/securesms/database/keys/InvalidKeyIdException.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.thoughtcrime.securesms.database;
+package org.thoughtcrime.securesms.database.keys;
public class InvalidKeyIdException extends Exception {
diff --git a/src/org/thoughtcrime/securesms/database/LocalKeyRecord.java b/src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java
similarity index 95%
rename from src/org/thoughtcrime/securesms/database/LocalKeyRecord.java
rename to src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java
index 67b84b8430..737b704c22 100644
--- a/src/org/thoughtcrime/securesms/database/LocalKeyRecord.java
+++ b/src/org/thoughtcrime/securesms/database/keys/LocalKeyRecord.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.thoughtcrime.securesms.database;
+package org.thoughtcrime.securesms.database.keys;
import android.content.Context;
import android.util.Log;
@@ -24,6 +24,9 @@ import org.thoughtcrime.securesms.crypto.KeyPair;
import org.thoughtcrime.securesms.crypto.KeyUtil;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.database.CanonicalAddressDatabase;
+import org.thoughtcrime.securesms.database.keys.InvalidKeyIdException;
+import org.thoughtcrime.securesms.database.keys.Record;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.io.FileInputStream;
diff --git a/src/org/thoughtcrime/securesms/database/Record.java b/src/org/thoughtcrime/securesms/database/keys/Record.java
similarity index 98%
rename from src/org/thoughtcrime/securesms/database/Record.java
rename to src/org/thoughtcrime/securesms/database/keys/Record.java
index 5f45ecae24..7c5b0d0603 100644
--- a/src/org/thoughtcrime/securesms/database/Record.java
+++ b/src/org/thoughtcrime/securesms/database/keys/Record.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.thoughtcrime.securesms.database;
+package org.thoughtcrime.securesms.database.keys;
import android.content.Context;
diff --git a/src/org/thoughtcrime/securesms/database/RemoteKeyRecord.java b/src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java
similarity index 95%
rename from src/org/thoughtcrime/securesms/database/RemoteKeyRecord.java
rename to src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java
index 5ab32aca94..71470bf70e 100644
--- a/src/org/thoughtcrime/securesms/database/RemoteKeyRecord.java
+++ b/src/org/thoughtcrime/securesms/database/keys/RemoteKeyRecord.java
@@ -14,13 +14,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.thoughtcrime.securesms.database;
+package org.thoughtcrime.securesms.database.keys;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.PublicKey;
+import org.thoughtcrime.securesms.database.CanonicalAddressDatabase;
+import org.thoughtcrime.securesms.database.keys.InvalidKeyIdException;
+import org.thoughtcrime.securesms.database.keys.Record;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Hex;
diff --git a/src/org/thoughtcrime/securesms/database/SessionKey.java b/src/org/thoughtcrime/securesms/database/keys/SessionKey.java
similarity index 98%
rename from src/org/thoughtcrime/securesms/database/SessionKey.java
rename to src/org/thoughtcrime/securesms/database/keys/SessionKey.java
index 2052bbe8cd..8d2d482f48 100644
--- a/src/org/thoughtcrime/securesms/database/SessionKey.java
+++ b/src/org/thoughtcrime/securesms/database/keys/SessionKey.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.thoughtcrime.securesms.database;
+package org.thoughtcrime.securesms.database.keys;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
diff --git a/src/org/thoughtcrime/securesms/database/SessionRecord.java b/src/org/thoughtcrime/securesms/database/keys/SessionRecord.java
similarity index 96%
rename from src/org/thoughtcrime/securesms/database/SessionRecord.java
rename to src/org/thoughtcrime/securesms/database/keys/SessionRecord.java
index f678aabced..acdc03b235 100644
--- a/src/org/thoughtcrime/securesms/database/SessionRecord.java
+++ b/src/org/thoughtcrime/securesms/database/keys/SessionRecord.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.thoughtcrime.securesms.database;
+package org.thoughtcrime.securesms.database.keys;
import android.content.Context;
import android.util.Log;
@@ -22,6 +22,9 @@ import android.util.Log;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.database.CanonicalAddressDatabase;
+import org.thoughtcrime.securesms.database.keys.Record;
+import org.thoughtcrime.securesms.database.keys.SessionKey;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.io.FileInputStream;
diff --git a/src/org/thoughtcrime/securesms/directory/BloomFilter.java b/src/org/thoughtcrime/securesms/directory/BloomFilter.java
new file mode 100644
index 0000000000..edbd05b67a
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/directory/BloomFilter.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.thoughtcrime.securesms.directory;
+
+import org.thoughtcrime.securesms.util.Conversions;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * A simple bloom filter implementation that backs the RedPhone directory.
+ *
+ * @author Moxie Marlinspike
+ *
+ */
+
+public class BloomFilter {
+
+ private final MappedByteBuffer buffer;
+ private final long length;
+ private final int hashCount;
+
+ public BloomFilter(File bloomFilter, int hashCount)
+ throws IOException
+ {
+ this.length = bloomFilter.length();
+ this.buffer = new FileInputStream(bloomFilter).getChannel()
+ .map(FileChannel.MapMode.READ_ONLY, 0, length);
+ this.hashCount = hashCount;
+ }
+
+ public int getHashCount() {
+ return hashCount;
+ }
+
+ private boolean isBitSet(long bitIndex) {
+ int byteInQuestion = this.buffer.get((int)(bitIndex / 8));
+ int bitOffset = (0x01 << (bitIndex % 8));
+
+ return (byteInQuestion & bitOffset) > 0;
+ }
+
+ public boolean contains(String entity) {
+ try {
+ for (int i=0;i.
+ */
+
+package org.thoughtcrime.securesms.directory;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.google.thoughtcrimegson.Gson;
+import com.google.thoughtcrimegson.JsonParseException;
+import com.google.thoughtcrimegson.annotations.SerializedName;
+import org.thoughtcrime.securesms.util.PhoneNumberFormatter;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+
+/**
+ * Handles providing lookups, serializing, and deserializing the RedPhone directory.
+ *
+ * @author Moxie Marlinspike
+ *
+ */
+
+public class NumberFilter {
+
+ private static NumberFilter instance;
+
+ public synchronized static NumberFilter getInstance(Context context) {
+ if (instance == null)
+ instance = NumberFilter.deserializeFromFile(context);
+
+ return instance;
+ }
+
+ private static final String DIRECTORY_META_FILE = "directory.stat";
+
+ private File bloomFilter;
+ private String version;
+ private long capacity;
+ private int hashCount;
+ private Context context;
+
+ private NumberFilter(Context context, File bloomFilter, long capacity,
+ int hashCount, String version)
+ {
+ this.context = context.getApplicationContext();
+ this.bloomFilter = bloomFilter;
+ this.capacity = capacity;
+ this.hashCount = hashCount;
+ this.version = version;
+ }
+
+ public synchronized boolean containsNumber(String number) {
+ try {
+ if (bloomFilter == null) return false;
+ else if (number == null || number.length() == 0) return false;
+
+ return new BloomFilter(bloomFilter, hashCount).contains(PhoneNumberFormatter.formatNumber(context, number));
+ } catch (IOException ioe) {
+ Log.w("NumberFilter", ioe);
+ return false;
+ }
+ }
+
+ public synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
+ {
+ if (this.bloomFilter != null)
+ this.bloomFilter.delete();
+
+ this.bloomFilter = bloomFilter;
+ this.capacity = capacity;
+ this.hashCount = hashCount;
+ this.version = version;
+
+ serializeToFile(context);
+ }
+
+ private void serializeToFile(Context context) {
+ if (this.bloomFilter == null)
+ return;
+
+ try {
+ FileOutputStream fout = context.openFileOutput(DIRECTORY_META_FILE, 0);
+ NumberFilterStorage storage = new NumberFilterStorage(bloomFilter.getAbsolutePath(),
+ capacity, hashCount, version);
+
+ storage.serializeToStream(fout);
+ fout.close();
+ } catch (IOException ioe) {
+ Log.w("NumberFilter", ioe);
+ }
+ }
+
+ private static NumberFilter deserializeFromFile(Context context) {
+ try {
+ FileInputStream fis = context.openFileInput(DIRECTORY_META_FILE);
+ NumberFilterStorage storage = NumberFilterStorage.fromStream(fis);
+
+ if (storage == null) return new NumberFilter(context, null, 0, 0, "0");
+ else return new NumberFilter(context,
+ new File(storage.getDataPath()),
+ storage.getCapacity(),
+ storage.getHashCount(),
+ storage.getVersion());
+ } catch (IOException ioe) {
+ Log.w("NumberFilter", ioe);
+ return new NumberFilter(context, null, 0, 0, "0");
+ }
+ }
+
+ private static class NumberFilterStorage {
+ @SerializedName("data_path")
+ private String dataPath;
+
+ @SerializedName("capacity")
+ private long capacity;
+
+ @SerializedName("hash_count")
+ private int hashCount;
+
+ @SerializedName("version")
+ private String version;
+
+ public NumberFilterStorage(String dataPath, long capacity, int hashCount, String version) {
+ this.dataPath = dataPath;
+ this.capacity = capacity;
+ this.hashCount = hashCount;
+ this.version = version;
+ }
+
+ public String getDataPath() {
+ return dataPath;
+ }
+
+ public long getCapacity() {
+ return capacity;
+ }
+
+ public int getHashCount() {
+ return hashCount;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void serializeToStream(OutputStream out) throws IOException {
+ out.write(new Gson().toJson(this).getBytes());
+ }
+
+ public static NumberFilterStorage fromStream(InputStream in) throws IOException {
+ try {
+ return new Gson().fromJson(new BufferedReader(new InputStreamReader(in)),
+ NumberFilterStorage.class);
+ } catch (JsonParseException jpe) {
+ Log.w("NumberFilter", jpe);
+ throw new IOException("JSON Parse Exception");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java b/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java
index 0cf01e64f9..c4ecc0230e 100644
--- a/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java
+++ b/src/org/thoughtcrime/securesms/gcm/GcmIntentService.java
@@ -4,10 +4,18 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
+import android.util.Log;
import com.google.android.gcm.GCMBaseIntentService;
+import com.google.thoughtcrimegson.Gson;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.service.RegistrationService;
+import org.thoughtcrime.securesms.service.SendReceiveService;
+import org.thoughtcrime.securesms.sms.TextMessage;
+import org.thoughtcrime.securesms.util.Util;
+
+import java.io.IOException;
+import java.util.ArrayList;
public class GcmIntentService extends GCMBaseIntentService {
@@ -21,25 +29,52 @@ public class GcmIntentService extends GCMBaseIntentService {
intent.putExtra(RegistrationService.GCM_REGISTRATION_ID, registrationId);
sendBroadcast(intent);
} else {
-//
-// // Talk to the server directly.
-//
+ try {
+ getGcmSocket(context).registerGcmId(registrationId);
+ } catch (IOException e) {
+ Log.w("GcmIntentService", e);
+ }
}
}
+ @Override
+ protected void onUnregistered(Context context, String registrationId) {
+ try {
+ getGcmSocket(context).unregisterGcmId(registrationId);
+ } catch (IOException ioe) {
+ Log.w("GcmIntentService", ioe);
+ }
+ }
+
+
@Override
protected void onMessage(Context context, Intent intent) {
- //To change body of implemented methods use File | Settings | File Templates.
+ Log.w("GcmIntentService", "Got GCM message!");
+ String data = intent.getStringExtra("message");
+ Log.w("GcmIntentService", "GCM message: " + data);
+
+ if (Util.isEmpty(data))
+ return;
+
+ IncomingGcmMessage message = new Gson().fromJson(data, IncomingGcmMessage.class);
+ ArrayList messages = new ArrayList();
+ messages.add(new TextMessage(message));
+
+ Intent receivedIntent = new Intent(context, SendReceiveService.class);
+ receivedIntent.setAction(SendReceiveService.RECEIVE_SMS_ACTION);
+ receivedIntent.putParcelableArrayListExtra("text_messages", messages);
+ context.startService(receivedIntent);
}
@Override
protected void onError(Context context, String s) {
- //To change body of implemented methods use File | Settings | File Templates.
+ Log.w("GcmIntentService", "GCM Error: " + s);
}
-
- @Override
- protected void onUnregistered(Context context, String s) {
- //To change body of implemented methods use File | Settings | File Templates.
+ private GcmSocket getGcmSocket(Context context) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String localNumber = preferences.getString(ApplicationPreferencesActivity.LOCAL_NUMBER_PREF, null);
+ String password = preferences.getString(ApplicationPreferencesActivity.GCM_PASSWORD_PREF, null);
+ return new GcmSocket(context, localNumber, password);
}
}
diff --git a/src/org/thoughtcrime/securesms/gcm/GcmMessageResponse.java b/src/org/thoughtcrime/securesms/gcm/GcmMessageResponse.java
new file mode 100644
index 0000000000..25c1ed787f
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/gcm/GcmMessageResponse.java
@@ -0,0 +1,18 @@
+package org.thoughtcrime.securesms.gcm;
+
+import java.util.List;
+
+public class GcmMessageResponse {
+ private List success;
+ private List failure;
+
+ public List getSuccess() {
+ return success;
+ }
+
+ public List getFailure() {
+ return failure;
+ }
+
+
+}
diff --git a/src/org/thoughtcrime/securesms/gcm/GcmSender.java b/src/org/thoughtcrime/securesms/gcm/GcmSender.java
deleted file mode 100644
index 0bf37ea063..0000000000
--- a/src/org/thoughtcrime/securesms/gcm/GcmSender.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.thoughtcrime.securesms.gcm;
-
-import android.app.PendingIntent;
-
-public class GcmSender {
-
- private static final GcmSender instance = new GcmSender();
-
- public static GcmSender getDefault() {
- return instance;
- }
-
- public void sendTextMessage(String recipient, String text,
- PendingIntent sentIntent,
- PendingIntent deliveredIntent)
- {
-
- }
-
-}
diff --git a/src/org/thoughtcrime/securesms/gcm/RegistrationSocket.java b/src/org/thoughtcrime/securesms/gcm/GcmSocket.java
similarity index 55%
rename from src/org/thoughtcrime/securesms/gcm/RegistrationSocket.java
rename to src/org/thoughtcrime/securesms/gcm/GcmSocket.java
index 6235abe999..a2df3584e9 100644
--- a/src/org/thoughtcrime/securesms/gcm/RegistrationSocket.java
+++ b/src/org/thoughtcrime/securesms/gcm/GcmSocket.java
@@ -3,13 +3,18 @@ package org.thoughtcrime.securesms.gcm;
import android.content.Context;
import android.content.res.AssetManager;
import android.util.Base64;
+import android.util.Log;
import com.google.thoughtcrimegson.Gson;
+import org.thoughtcrime.securesms.directory.DirectoryDescriptor;
+import org.thoughtcrime.securesms.directory.NumberFilter;
import org.thoughtcrime.securesms.util.Util;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -21,18 +26,21 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
+import java.util.zip.GZIPInputStream;
-public class RegistrationSocket {
+public class GcmSocket {
private static final String CREATE_ACCOUNT_PATH = "/v1/accounts/%s";
private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/%s";
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/%s";
+ private static final String DIRECTORY_PATH = "/v1/directory/";
+ private static final String MESSAGE_PATH = "/v1/messages/";
private final String localNumber;
private final String password;
private final TrustManagerFactory trustManagerFactory;
- public RegistrationSocket(Context context, String localNumber, String password) {
+ public GcmSocket(Context context, String localNumber, String password) {
this.localNumber = localNumber;
this.password = password;
this.trustManagerFactory = initializeTrustManagerFactory(context);
@@ -54,6 +62,118 @@ public class RegistrationSocket {
makeRequest(String.format(REGISTER_GCM_PATH, localNumber), "PUT", new Gson().toJson(registration));
}
+ public void unregisterGcmId(String gcmRegistrationId) throws IOException {
+ GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
+ makeRequest(String.format(REGISTER_GCM_PATH, localNumber), "DELETE", new Gson().toJson(registration));
+ }
+
+ public void sendMessage(String recipient, String messageText) throws IOException {
+ OutgoingGcmMessage message = new OutgoingGcmMessage(recipient, messageText);
+ String responseText = makeRequest(MESSAGE_PATH, "POST", new Gson().toJson(message));
+ GcmMessageResponse response = new Gson().fromJson(responseText, GcmMessageResponse.class);
+
+ if (response.getFailure().size() != 0)
+ throw new IOException("Got send failure: " + response.getFailure().get(0));
+ }
+
+ public void retrieveDirectory(Context context ) {
+ try {
+ DirectoryDescriptor directoryDescriptor = new Gson().fromJson(makeRequest(DIRECTORY_PATH, "GET", null),
+ DirectoryDescriptor.class);
+
+ File directoryData = downloadExternalFile(context, directoryDescriptor.getUrl());
+
+ NumberFilter.getInstance(context).update(directoryData,
+ directoryDescriptor.getCapacity(),
+ directoryDescriptor.getHashCount(),
+ directoryDescriptor.getVersion());
+
+ } catch (IOException ioe) {
+ Log.w("GcmSocket", ioe);
+ }
+ }
+
+ private File downloadExternalFile(Context context, String url) throws IOException {
+ File download = File.createTempFile("directory", ".dat", context.getFilesDir());
+ URL downloadUrl = new URL(url);
+ HttpsURLConnection connection = (HttpsURLConnection)downloadUrl.openConnection();
+ connection.setDoInput(true);
+
+ if (connection.getResponseCode() != 200) {
+ throw new IOException("Bad response: " + connection.getResponseCode());
+ }
+
+ OutputStream output = new FileOutputStream(download);
+ InputStream input = new GZIPInputStream(connection.getInputStream());
+ int read = 0;
+ byte[] buffer = new byte[4096];
+
+ while ((read = input.read(buffer)) != -1) {
+ output.write(buffer, 0, read);
+ }
+
+ output.close();
+
+ return download;
+ }
+
+ private String makeRequest(String urlFragment, String method, String body) throws IOException {
+ HttpsURLConnection connection = getConnection(urlFragment, method);
+
+ if (body != null) {
+ connection.setDoOutput(true);
+ }
+
+ connection.connect();
+
+ if (body != null) {
+ Log.w("GcmSocket", method + " -- " + body);
+ OutputStream out = connection.getOutputStream();
+ out.write(body.getBytes());
+ out.close();
+ }
+
+ if (connection.getResponseCode() != 200) {
+ throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
+ }
+
+ return Util.readFully(connection.getInputStream());
+ }
+
+ private HttpsURLConnection getConnection(String urlFragment, String method) throws IOException {
+ try {
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, trustManagerFactory.getTrustManagers(), null);
+
+ URL url = new URL(String.format("https://gcm.textsecure.whispersystems.org%s", urlFragment));
+ HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+ connection.setSSLSocketFactory(context.getSocketFactory());
+ connection.setRequestMethod(method);
+ connection.setRequestProperty("Content-Type", "application/json");
+
+ if (password != null) {
+ System.out.println("Adding authorization header: " + getAuthorizationHeader());
+ connection.setRequestProperty("Authorization", getAuthorizationHeader());
+ }
+
+ return connection;
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError(e);
+ } catch (KeyManagementException e) {
+ throw new AssertionError(e);
+ } catch (MalformedURLException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private String getAuthorizationHeader() {
+ try {
+ return "Basic " + new String(Base64.encode((localNumber + ":" + password).getBytes("UTF-8"), Base64.NO_WRAP));
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
private TrustManagerFactory initializeTrustManagerFactory(Context context) {
try {
AssetManager assetManager = context.getAssets();
@@ -77,54 +197,6 @@ public class RegistrationSocket {
}
}
- private String makeRequest(String urlFragment, String method, String body) throws IOException {
- try {
- SSLContext context = SSLContext.getInstance("TLS");
- context.init(null, trustManagerFactory.getTrustManagers(), null);
-
- URL url = new URL(String.format("https://gcm.textsecure.whispersystems.org/%s", urlFragment));
- HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
- connection.setSSLSocketFactory(context.getSocketFactory());
- connection.setRequestMethod(method);
- connection.setRequestProperty("Content-Type", "application/json");
-
- if (password != null) {
- connection.setRequestProperty("Authorization", getAuthorizationHeader());
- }
-
- if (body != null) {
- connection.setDoOutput(true);
- }
-
- connection.connect();
-
- if (body != null) {
- OutputStream out = connection.getOutputStream();
- out.write(body.getBytes());
- out.close();
- }
-
- if (connection.getResponseCode() != 200) {
- throw new IOException("Bad response: " + connection.getResponseCode());
- }
-
- return Util.readFully(connection.getInputStream());
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e);
- } catch (KeyManagementException e) {
- throw new AssertionError(e);
- } catch (MalformedURLException e) {
- throw new AssertionError(e);
- }
- }
-
- private String getAuthorizationHeader() {
- try {
- return "Basic " + new String(Base64.encode((localNumber + ":" + password).getBytes("UTF-8"), Base64.NO_WRAP));
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError(e);
- }
- }
private class Verification {
diff --git a/src/org/thoughtcrime/securesms/gcm/IncomingGcmMessage.java b/src/org/thoughtcrime/securesms/gcm/IncomingGcmMessage.java
new file mode 100644
index 0000000000..9cc40bcdd0
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/gcm/IncomingGcmMessage.java
@@ -0,0 +1,42 @@
+package org.thoughtcrime.securesms.gcm;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class IncomingGcmMessage {
+
+ private String source;
+ private List destinations;
+ private String messageText;
+ private List attachments;
+ private long timestamp;
+
+ public IncomingGcmMessage(String source, List destinations, String messageText, List attachments, long timestamp) {
+ this.source = source;
+ this.destinations = destinations;
+ this.messageText = messageText;
+ this.attachments = attachments;
+ this.timestamp = timestamp;
+ }
+
+ public long getTimestampMillis() {
+ return timestamp;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public List getAttachments() {
+ return attachments;
+ }
+
+ public String getMessageText() {
+ return messageText;
+ }
+
+ public List getDestinations() {
+ return destinations;
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/gcm/OptimizingTransport.java b/src/org/thoughtcrime/securesms/gcm/OptimizingTransport.java
new file mode 100644
index 0000000000..10a8f8be46
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/gcm/OptimizingTransport.java
@@ -0,0 +1,79 @@
+package org.thoughtcrime.securesms.gcm;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.telephony.SmsManager;
+import android.util.Log;
+
+import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
+import org.thoughtcrime.securesms.directory.NumberFilter;
+import org.thoughtcrime.securesms.util.PhoneNumberFormatter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class OptimizingTransport {
+
+ public static void sendTextMessage(Context context, String destinationAddress, String message,
+ PendingIntent sentIntent, PendingIntent deliveredIntent)
+ {
+ Log.w("OptimzingTransport", "Outgoing message: " + PhoneNumberFormatter.formatNumber(context, destinationAddress));
+ NumberFilter filter = NumberFilter.getInstance(context);
+
+ if (filter.containsNumber(PhoneNumberFormatter.formatNumber(context, destinationAddress))) {
+ Log.w("OptimzingTransport", "In the filter, sending GCM...");
+ sendGcmTextMessage(context, destinationAddress, message, sentIntent, deliveredIntent);
+ } else {
+ Log.w("OptimzingTransport", "Not in the filter, sending SMS...");
+ sendSmsTextMessage(destinationAddress, message, sentIntent, deliveredIntent);
+ }
+ }
+
+ public static void sendMultipartTextMessage(Context context,
+ String recipient,
+ ArrayList messages,
+ ArrayList sentIntents,
+ ArrayList deliveredIntents)
+ {
+ // FIXME
+
+ sendTextMessage(context, recipient, messages.get(0), sentIntents.get(0), deliveredIntents == null ? null : deliveredIntents.get(0));
+ }
+
+
+ private static void sendGcmTextMessage(Context context, String recipient, String messageText,
+ PendingIntent sentIntent, PendingIntent deliveredIntent)
+ {
+ try {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String localNumber = preferences.getString(ApplicationPreferencesActivity.LOCAL_NUMBER_PREF, null);
+ String password = preferences.getString(ApplicationPreferencesActivity.GCM_PASSWORD_PREF, null);
+
+ if (localNumber == null || password == null) {
+ Log.w("OptimzingTransport", "No credentials, falling back to SMS...");
+ sendSmsTextMessage(recipient, messageText, sentIntent, deliveredIntent);
+ return;
+ }
+
+ GcmSocket gcmSocket = new GcmSocket(context, localNumber, password);
+ gcmSocket.sendMessage(PhoneNumberFormatter.formatNumber(context, recipient), messageText);
+ sentIntent.send(Activity.RESULT_OK);
+ } catch (IOException ioe) {
+ Log.w("OptimizingTransport", ioe);
+ Log.w("OptimzingTransport", "IOException, falling back to SMS...");
+ sendSmsTextMessage(recipient, messageText, sentIntent, deliveredIntent);
+ } catch (PendingIntent.CanceledException e) {
+ Log.w("OptimizingTransport", e);
+ }
+ }
+
+ private static void sendSmsTextMessage(String recipient, String message,
+ PendingIntent sentIntent, PendingIntent deliveredIntent)
+ {
+ SmsManager.getDefault().sendTextMessage(recipient, null, message, sentIntent, deliveredIntent);
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/gcm/OutgoingGcmMessage.java b/src/org/thoughtcrime/securesms/gcm/OutgoingGcmMessage.java
new file mode 100644
index 0000000000..911222f1c0
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/gcm/OutgoingGcmMessage.java
@@ -0,0 +1,37 @@
+package org.thoughtcrime.securesms.gcm;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class OutgoingGcmMessage {
+
+ private List destinations;
+ private String messageText;
+ private List attachments;
+
+ public OutgoingGcmMessage(List destinations, String messageText, List attachments) {
+ this.destinations = destinations;
+ this.messageText = messageText;
+ this.attachments = attachments;
+ }
+
+ public OutgoingGcmMessage(String destination, String messageText) {
+ this.destinations = new LinkedList();
+ this.destinations.add(destination);
+ this.messageText = messageText;
+ this.attachments = new LinkedList();
+ }
+
+ public List getDestinations() {
+ return destinations;
+ }
+
+ public String getMessageText() {
+ return messageText;
+ }
+
+ public List getAttachments() {
+ return attachments;
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/service/RegistrationService.java b/src/org/thoughtcrime/securesms/service/RegistrationService.java
index 8e463ba52f..afd94b3fc7 100644
--- a/src/org/thoughtcrime/securesms/service/RegistrationService.java
+++ b/src/org/thoughtcrime/securesms/service/RegistrationService.java
@@ -17,7 +17,7 @@ import com.google.android.gcm.GCMRegistrar;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.gcm.GcmIntentService;
import org.thoughtcrime.securesms.gcm.GcmRegistrationTimeoutException;
-import org.thoughtcrime.securesms.gcm.RegistrationSocket;
+import org.thoughtcrime.securesms.gcm.GcmSocket;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
@@ -127,14 +127,14 @@ public class RegistrationService extends Service {
registerReceiver(gcmRegistrationReceiver, filter);
}
- private void shutdownChallengeListener() {
+ private synchronized void shutdownChallengeListener() {
if (challengeReceiver != null) {
unregisterReceiver(challengeReceiver);
challengeReceiver = null;
}
}
- private void shutdownGcmRegistrationListener() {
+ private synchronized void shutdownGcmRegistrationListener() {
if (gcmRegistrationReceiver != null) {
unregisterReceiver(gcmRegistrationReceiver);
gcmRegistrationReceiver = null;
@@ -144,7 +144,7 @@ public class RegistrationService extends Service {
private void handleRegistrationIntent(Intent intent) {
markAsVerifying(true);
- RegistrationSocket socket;
+ GcmSocket socket;
String number = intent.getStringExtra("e164number");
try {
@@ -153,7 +153,7 @@ public class RegistrationService extends Service {
initializeGcmRegistrationListener();
setState(new RegistrationState(RegistrationState.STATE_CONNECTING, number));
- socket = new RegistrationSocket(this, number, password);
+ socket = new GcmSocket(this, number, password);
socket.createAccount();
setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number));
@@ -165,6 +165,9 @@ public class RegistrationService extends Service {
String gcmRegistrationId = waitForGcmRegistrationId();
socket.registerGcmId(gcmRegistrationId);
+ setState(new RegistrationState(RegistrationState.STATE_RETRIEVING_DIRECTORY, number));
+ socket.retrieveDirectory(this);
+
markAsVerified(number, password);
setState(new RegistrationState(RegistrationState.STATE_COMPLETE, number));
@@ -315,6 +318,8 @@ public class RegistrationService extends Service {
public static final int STATE_GCM_REGISTERING = 9;
public static final int STATE_GCM_TIMEOUT = 10;
+ public static final int STATE_RETRIEVING_DIRECTORY = 11;
+
public final int state;
public final String number;
diff --git a/src/org/thoughtcrime/securesms/service/SendReceiveService.java b/src/org/thoughtcrime/securesms/service/SendReceiveService.java
index a4891fdf70..ffbcb41cfe 100644
--- a/src/org/thoughtcrime/securesms/service/SendReceiveService.java
+++ b/src/org/thoughtcrime/securesms/service/SendReceiveService.java
@@ -63,11 +63,11 @@ public class SendReceiveService extends Service {
private ToastHandler toastHandler;
- private SmsReceiver smsReceiver;
- private SmsSender smsSender;
- private MmsReceiver mmsReceiver;
- private MmsSender mmsSender;
- private MmsDownloader mmsDownloader;
+ private SmsReceiver smsReceiver;
+ private SmsSender smsSender;
+ private MmsReceiver mmsReceiver;
+ private MmsSender mmsSender;
+ private MmsDownloader mmsDownloader;
private MasterSecret masterSecret;
private boolean hasSecret;
diff --git a/src/org/thoughtcrime/securesms/service/SmsListener.java b/src/org/thoughtcrime/securesms/service/SmsListener.java
index 2648d3a1e7..4e720da3a8 100644
--- a/src/org/thoughtcrime/securesms/service/SmsListener.java
+++ b/src/org/thoughtcrime/securesms/service/SmsListener.java
@@ -27,6 +27,9 @@ import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.protocol.WirePrefix;
+import org.thoughtcrime.securesms.sms.TextMessage;
+
+import java.util.ArrayList;
public class SmsListener extends BroadcastReceiver {
@@ -74,6 +77,16 @@ public class SmsListener extends BroadcastReceiver {
return bodyBuilder.toString();
}
+ private ArrayList getAsTextMessages(Intent intent) {
+ Object[] pdus = (Object[])intent.getExtras().get("pdus");
+ ArrayList messages = new ArrayList(pdus.length);
+
+ for (int i=0;i messagesList = intent.getExtras().getParcelableArrayList("text_messages");
+ TextMessage[] messages = messagesList.toArray(new TextMessage[0]);
+// Bundle bundle = intent.getExtras();
+// SmsMessage[] messages = parseMessages(bundle);
String message = assembleMessageFragments(messages);
if (message != null) {
diff --git a/src/org/thoughtcrime/securesms/service/SmsSender.java b/src/org/thoughtcrime/securesms/service/SmsSender.java
index c2e3071ca7..fbe6ef1fd9 100644
--- a/src/org/thoughtcrime/securesms/service/SmsSender.java
+++ b/src/org/thoughtcrime/securesms/service/SmsSender.java
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SessionCipher;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.SmsDatabase;
+import org.thoughtcrime.securesms.gcm.OptimizingTransport;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.KeyExchangeWirePrefix;
import org.thoughtcrime.securesms.protocol.Prefix;
@@ -224,7 +225,9 @@ public class SmsSender {
// the message as a failure. That way at least it doesn't repeatedly crash every time you start
// the app.
try {
- SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
+ OptimizingTransport.sendMultipartTextMessage(context, recipient, messages, sentIntents, deliveredIntents);
+//
+// SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
} catch (NullPointerException npe) {
Log.w("SmsSender", npe);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
@@ -257,8 +260,10 @@ public class SmsSender {
// the message as a failure. That way at least it doesn't repeatedly crash every time you start
// the app.
try {
- SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i), sentIntents.get(i),
- deliveredIntents == null ? null : deliveredIntents.get(i));
+ OptimizingTransport.sendTextMessage(context, recipient, messages.get(i), sentIntents.get(i),
+ deliveredIntents == null ? null : deliveredIntents.get(i));
+// SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i), sentIntents.get(i),
+// deliveredIntents == null ? null : deliveredIntents.get(i));
} catch (NullPointerException npe) {
Log.w("SmsSender", npe);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
diff --git a/src/org/thoughtcrime/securesms/sms/TextMessage.java b/src/org/thoughtcrime/securesms/sms/TextMessage.java
new file mode 100644
index 0000000000..11a5cba534
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/sms/TextMessage.java
@@ -0,0 +1,104 @@
+package org.thoughtcrime.securesms.sms;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.SmsMessage;
+
+import org.thoughtcrime.securesms.gcm.IncomingGcmMessage;
+
+public class TextMessage implements Parcelable {
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ @Override
+ public TextMessage createFromParcel(Parcel in) {
+ return new TextMessage(in);
+ }
+
+ @Override
+ public TextMessage[] newArray(int size) {
+ return new TextMessage[size];
+ }
+ };
+
+ private final String message;
+ private final String sender;
+ private final int protocol;
+ private final String serviceCenterAddress;
+ private final boolean replyPathPresent;
+ private final String pseudoSubject;
+ private final long sentTimestampMillis;
+
+ public TextMessage(SmsMessage message) {
+ this.message = message.getDisplayMessageBody();
+ this.sender = message.getDisplayOriginatingAddress();
+ this.protocol = message.getProtocolIdentifier();
+ this.serviceCenterAddress = message.getServiceCenterAddress();
+ this.replyPathPresent = message.isReplyPathPresent();
+ this.pseudoSubject = message.getPseudoSubject();
+ this.sentTimestampMillis = message.getTimestampMillis();
+ }
+
+ public TextMessage(IncomingGcmMessage message) {
+ this.message = message.getMessageText();
+ this.sender = message.getSource();
+ this.protocol = 31337;
+ this.serviceCenterAddress = "GCM";
+ this.replyPathPresent = true;
+ this.pseudoSubject = "";
+ this.sentTimestampMillis = message.getTimestampMillis();
+ }
+
+ public TextMessage(Parcel in) {
+ this.message = in.readString();
+ this.sender = in.readString();
+ this.protocol = in.readInt();
+ this.serviceCenterAddress = in.readString();
+ this.replyPathPresent = (in.readInt() == 1);
+ this.pseudoSubject = in.readString();
+ this.sentTimestampMillis = in.readLong();
+ }
+
+ public long getSentTimestampMillis() {
+ return sentTimestampMillis;
+ }
+
+ public String getPseudoSubject() {
+ return pseudoSubject;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public String getSender() {
+ return sender;
+ }
+
+ public int getProtocol() {
+ return protocol;
+ }
+
+ public String getServiceCenterAddress() {
+ return serviceCenterAddress;
+ }
+
+ public boolean isReplyPathPresent() {
+ return replyPathPresent;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(message);
+ out.writeString(sender);
+ out.writeInt(protocol);
+ out.writeString(serviceCenterAddress);
+ out.writeInt(replyPathPresent ? 1 : 0);
+ out.writeString(pseudoSubject);
+ out.writeLong(sentTimestampMillis);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/util/Conversions.java b/src/org/thoughtcrime/securesms/util/Conversions.java
index 91aede2eb8..561bf73ca5 100644
--- a/src/org/thoughtcrime/securesms/util/Conversions.java
+++ b/src/org/thoughtcrime/securesms/util/Conversions.java
@@ -158,6 +158,14 @@ public class Conversions {
return byteArrayToLong(bytes, 0);
}
+ public static long byteArray4ToLong(byte[] bytes, int offset) {
+ return
+ ((bytes[offset + 0] & 0xffL) << 24) |
+ ((bytes[offset + 1] & 0xffL) << 16) |
+ ((bytes[offset + 2] & 0xffL) << 8) |
+ ((bytes[offset + 3] & 0xffL));
+ }
+
public static long byteArrayToLong(byte[] bytes, int offset) {
return
((bytes[offset] & 0xffL) << 56) |