Initial client support for GCM message send/receive
parent
2f39283da3
commit
303d1acd45
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<this.hashCount;i++) {
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(new SecretKeySpec((i+"").getBytes(), "HmacSHA1"));
|
||||
|
||||
byte[] hashValue = mac.doFinal(entity.getBytes());
|
||||
long bitIndex = Math.abs(Conversions.byteArrayToLong(hashValue, 0)) % (this.length * 8);
|
||||
|
||||
if (!isBitSet(bitIndex))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package org.thoughtcrime.securesms.directory;
|
||||
|
||||
public class DirectoryDescriptor {
|
||||
private String version;
|
||||
private long capacity;
|
||||
private int hashCount;
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public int getHashCount() {
|
||||
return hashCount;
|
||||
}
|
||||
|
||||
public long getCapacity() {
|
||||
return capacity;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
private String url;
|
||||
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package org.thoughtcrime.securesms.gcm;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GcmMessageResponse {
|
||||
private List<String> success;
|
||||
private List<String> failure;
|
||||
|
||||
public List<String> getSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public List<String> getFailure() {
|
||||
return failure;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<String> destinations;
|
||||
private String messageText;
|
||||
private List<String> attachments;
|
||||
private long timestamp;
|
||||
|
||||
public IncomingGcmMessage(String source, List<String> destinations, String messageText, List<String> 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<String> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public String getMessageText() {
|
||||
return messageText;
|
||||
}
|
||||
|
||||
public List<String> getDestinations() {
|
||||
return destinations;
|
||||
}
|
||||
|
||||
}
|
@ -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<String> messages,
|
||||
ArrayList<PendingIntent> sentIntents,
|
||||
ArrayList<PendingIntent> 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package org.thoughtcrime.securesms.gcm;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingGcmMessage {
|
||||
|
||||
private List<String> destinations;
|
||||
private String messageText;
|
||||
private List<String> attachments;
|
||||
|
||||
public OutgoingGcmMessage(List<String> destinations, String messageText, List<String> attachments) {
|
||||
this.destinations = destinations;
|
||||
this.messageText = messageText;
|
||||
this.attachments = attachments;
|
||||
}
|
||||
|
||||
public OutgoingGcmMessage(String destination, String messageText) {
|
||||
this.destinations = new LinkedList<String>();
|
||||
this.destinations.add(destination);
|
||||
this.messageText = messageText;
|
||||
this.attachments = new LinkedList<String>();
|
||||
}
|
||||
|
||||
public List<String> getDestinations() {
|
||||
return destinations;
|
||||
}
|
||||
|
||||
public String getMessageText() {
|
||||
return messageText;
|
||||
}
|
||||
|
||||
public List<String> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
}
|
@ -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<TextMessage> CREATOR = new Parcelable.Creator<TextMessage>() {
|
||||
@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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue