Handle SMS fallback preferences correctly, and fix directory sync.

This commit is contained in:
Moxie Marlinspike 2014-02-17 15:31:42 -08:00
parent 94b54a6d63
commit 9bb327db42
16 changed files with 148 additions and 151 deletions

View File

@ -7,11 +7,9 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.telephony.TelephonyManager;
import android.util.Log;
import org.whispersystems.textsecure.push.ContactNumberDetails;
import org.whispersystems.textsecure.util.DirectoryUtil;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.util.InvalidNumberException;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
@ -19,7 +17,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Directory {
@ -66,6 +63,25 @@ public class Directory {
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public boolean isSmsFallbackSupported(String e164number) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, new String[] {SUPPORTS_SMS}, NUMBER + " = ?",
new String[]{e164number}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getInt(0) == 1;
} else {
return false;
}
} finally {
if (cursor != null)
cursor.close();
}
}
public boolean isActiveNumber(String e164number) throws NotInDirectoryException {
if (e164number == null || e164number.length() == 0) {
return false;
@ -109,7 +125,7 @@ public class Directory {
}
}
public void setNumber(ContactNumberDetails token, boolean active) {
public void setNumber(ContactTokenDetails token, boolean active) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(NUMBER, token.getNumber());
@ -120,13 +136,13 @@ public class Directory {
db.replace(TABLE_NAME, null, values);
}
public void setNumbers(List<ContactNumberDetails> activeTokens, Collection<String> inactiveTokens) {
public void setNumbers(List<ContactTokenDetails> activeTokens, Collection<String> inactiveTokens) {
long timestamp = System.currentTimeMillis();
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
for (ContactNumberDetails token : activeTokens) {
for (ContactTokenDetails token : activeTokens) {
Log.w("Directory", "Adding active token: " + token);
ContentValues values = new ContentValues();
values.put(NUMBER, token.getNumber());

View File

@ -0,0 +1,22 @@
package org.whispersystems.textsecure.push;
public class AccountAttributes {
private String signalingKey;
private boolean supportsSms;
public AccountAttributes(String signalingKey, boolean supportsSms) {
this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
}
public AccountAttributes() {}
public String getSignalingKey() {
return signalingKey;
}
public boolean isSupportsSms() {
return supportsSms;
}
}

View File

@ -1,31 +0,0 @@
package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.Gson;
public abstract class ContactDetails {
private String relay;
private boolean supportsSms;
public ContactDetails() {}
public ContactDetails(String relay) {
this.relay = relay;
}
public String getRelay() {
return relay;
}
public void setRelay(String relay) {
this.relay = relay;
}
public boolean isSupportsSms() {
return supportsSms;
}
public String toString() {
return new Gson().toJson(this);
}
}

View File

@ -1,42 +0,0 @@
package org.whispersystems.textsecure.push;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ContactNumberDetails extends ContactDetails {
private static final String TAG = "ContactNumberDetails";
private String number;
public ContactNumberDetails() { super(); }
public ContactNumberDetails(String number) {
super();
this.number = number;
}
public ContactNumberDetails(String number, String relay) {
super(relay);
this.number = number;
}
public String getNumber() {
return number;
}
public static List<ContactNumberDetails> fromContactTokenDetailsList(List<ContactTokenDetails> contactTokenDetails, final Map<String, String> tokenMap) {
if (contactTokenDetails == null || tokenMap == null) return null;
List<ContactNumberDetails> contactNumberDetails = new ArrayList<ContactNumberDetails>(contactTokenDetails.size());
for (ContactTokenDetails tokenDetails : contactTokenDetails) {
if (tokenMap.containsKey(tokenDetails.getToken()))
contactNumberDetails.add(new ContactNumberDetails(tokenMap.get(tokenDetails.getToken()), tokenDetails.getRelay()));
else
Log.w(TAG, "tokenMap was missing a contact.");
}
return contactNumberDetails;
}
}

View File

@ -2,24 +2,32 @@ package org.whispersystems.textsecure.push;
import com.google.thoughtcrimegson.Gson;
public class ContactTokenDetails extends ContactDetails {
public class ContactTokenDetails {
private String token;
private String relay;
private String number;
private boolean supportsSms;
public ContactTokenDetails() { super(); }
public ContactTokenDetails(String token) {
super();
this.token = token;
}
public ContactTokenDetails(String token, String relay) {
super(relay);
this.token = token;
}
public ContactTokenDetails() {}
public String getToken() {
return token;
}
public String getRelay() {
return relay;
}
public boolean isSupportsSms() {
return supportsSms;
}
public void setNumber(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
}

View File

@ -71,8 +71,10 @@ public class PushServiceSocket {
makeRequest(String.format(path, localNumber), "GET", null);
}
public void verifyAccount(String verificationCode, String signalingKey) throws IOException {
SignalingKey signalingKeyEntity = new SignalingKey(signalingKey);
public void verifyAccount(String verificationCode, String signalingKey, boolean supportsSms)
throws IOException
{
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms);
makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode),
"PUT", new Gson().toJson(signalingKeyEntity));
}

View File

@ -1,16 +0,0 @@
package org.whispersystems.textsecure.push;
public class SignalingKey {
private String signalingKey;
public SignalingKey(String signalingKey) {
this.signalingKey = signalingKey;
}
public SignalingKey() {}
public String getSignalingKey() {
return signalingKey;
}
}

View File

@ -501,7 +501,7 @@ public class RegistrationProgressActivity extends SherlockActivity {
protected Integer doInBackground(Void... params) {
try {
PushServiceSocket socket = PushServiceSocketFactory.create(context, e164number, password);
socket.verifyAccount(code, signalingKey);
socket.verifyAccount(code, signalingKey, true);
return SUCCESS;
} catch (RateLimitException e) {
Log.w("RegistrationProgressActivity", e);

View File

@ -6,7 +6,6 @@ import android.util.Log;
import com.google.android.gcm.GCMBaseIntentService;
import org.thoughtcrime.securesms.Release;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.service.RegistrationService;
import org.thoughtcrime.securesms.service.SendReceiveService;
@ -14,7 +13,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.InvalidVersionException;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactNumberDetails;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.IncomingEncryptedPushMessage;
import org.whispersystems.textsecure.push.IncomingPushMessage;
@ -69,9 +67,10 @@ public class GcmIntentService extends GCMBaseIntentService {
if (!isActiveNumber(context, message.getSource())) {
Directory directory = Directory.getInstance(context);
String contactNumber = message.getSource();
ContactNumberDetails contactNumberDetails = new ContactNumberDetails(contactNumber, message.getRelay());
directory.setNumber(contactNumberDetails, true);
ContactTokenDetails contactTokenDetails = new ContactTokenDetails();
contactTokenDetails.setNumber(message.getSource());
directory.setNumber(contactTokenDetails, true);
}
Intent service = new Intent(context, SendReceiveService.class);

View File

@ -242,7 +242,7 @@ public class RegistrationService extends Service {
setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number));
String challenge = waitForChallenge();
socket.verifyAccount(challenge, signalingKey);
socket.verifyAccount(challenge, signalingKey, true);
handleCommonRegistration(masterSecret, socket, number);
markAsVerified(number, password, signalingKey);

View File

@ -17,6 +17,8 @@
package org.thoughtcrime.securesms.service;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.telephony.SmsManager;
@ -31,12 +33,11 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.transport.UniversalTransport;
import org.thoughtcrime.securesms.transport.UntrustedIdentityException;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.util.Base64;
public class SmsSender {
@ -87,6 +88,10 @@ public class SmsSender {
} catch (UndeliverableMessageException ude) {
Log.w("SmsSender", ude);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
} catch (RetryLaterException rle) {
Log.w("SmsSender", rle);
if (systemStateListener.isConnected()) scheduleQuickRetryAlarm();
else systemStateListener.registerForConnectivityChange();
}
}
} finally {
@ -140,10 +145,20 @@ public class SmsSender {
}
private void registerForRadioChanges() {
systemStateListener.registerForConnectivityChange();
if (systemStateListener.isConnected()) systemStateListener.registerForRadioChange();
else systemStateListener.registerForConnectivityChange();
}
private void unregisterForRadioChanges() {
systemStateListener.unregisterForConnectivityChange();
}
private void scheduleQuickRetryAlarm() {
((AlarmManager)context.getSystemService(Context.ALARM_SERVICE))
.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (30 * 1000),
PendingIntent.getService(context, 0,
new Intent(SendReceiveService.SEND_SMS_ACTION,
null, context, SendReceiveService.class),
PendingIntent.FLAG_UPDATE_CURRENT));
}
}

View File

@ -24,7 +24,15 @@ public class SystemStateListener {
this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
public void registerForRadioChange() {
Log.w("SystemStateListener", "Registering for radio changes...");
unregisterForConnectivityChange();
telephonyManager.listen(telephonyListener, PhoneStateListener.LISTEN_SERVICE_STATE);
}
public void registerForConnectivityChange() {
Log.w("SystemStateListener", "Registering for any connectivity changes...");
unregisterForConnectivityChange();
telephonyManager.listen(telephonyListener, PhoneStateListener.LISTEN_SERVICE_STATE);
@ -63,6 +71,7 @@ public class SystemStateListener {
@Override
public void onServiceStateChanged(ServiceState state) {
if (state.getState() == ServiceState.STATE_IN_SERVICE) {
Log.w("SystemStateListener", "In service, sending sms/mms outboxes...");
sendSmsOutbox(context);
sendMmsOutbox(context);
}
@ -76,6 +85,7 @@ public class SystemStateListener {
if (connectivityManager.getActiveNetworkInfo() != null &&
connectivityManager.getActiveNetworkInfo().isConnected())
{
Log.w("SystemStateListener", "Got connectivity action: " + intent.toString());
sendSmsOutbox(context);
sendMmsOutbox(context);
}

View File

@ -91,11 +91,6 @@ public class PushTransport extends BaseTransport {
context.sendBroadcast(constructSentIntent(context, message.getId(), message.getType(), true));
} catch (UnregisteredUserException e) {
Log.w("PushTransport", e);
//TODO We should probably remove the user from the directory?
// destroySessions(message.getIndividualRecipient());
throw new IOException("Not push registered after all.");
} catch (InvalidNumberException e) {
Log.w("PushTransport", e);
throw new IOException("Badly formatted number.");

View File

@ -24,8 +24,6 @@ import android.util.Log;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler;
import org.thoughtcrime.securesms.sms.OutgoingPrekeyBundleMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;

View File

@ -33,13 +33,9 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactNumberDetails;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.IncomingPushMessage;
import org.whispersystems.textsecure.push.PushMessageProtos;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.DirectoryUtil;
import org.whispersystems.textsecure.util.InvalidNumberException;
@ -47,8 +43,6 @@ import java.io.IOException;
import ws.com.google.android.mms.pdu.SendReq;
import static org.whispersystems.textsecure.push.PushMessageProtos.IncomingPushMessageSignal;
public class UniversalTransport {
private final Context context;
@ -66,7 +60,7 @@ public class UniversalTransport {
}
public void deliver(SmsMessageRecord message)
throws UndeliverableMessageException, UntrustedIdentityException
throws UndeliverableMessageException, UntrustedIdentityException, RetryLaterException
{
if (!TextSecurePreferences.isPushRegistered(context)) {
smsTransport.deliver(message);
@ -78,12 +72,19 @@ public class UniversalTransport {
String number = Util.canonicalizeNumber(context, recipient.getNumber());
if (isPushTransport(number)) {
boolean isSmsFallbackSupported = isSmsFallbackSupported(number);
try {
Log.w("UniversalTransport", "Delivering with GCM...");
pushTransport.deliver(message);
} catch (UnregisteredUserException uue) {
Log.w("UnviersalTransport", uue);
if (isSmsFallbackSupported) smsTransport.deliver(message);
else throw new UndeliverableMessageException(uue);
} catch (IOException ioe) {
Log.w("UniversalTransport", ioe);
smsTransport.deliver(message);
if (isSmsFallbackSupported) smsTransport.deliver(message);
else throw new RetryLaterException(ioe);
}
} else {
Log.w("UniversalTransport", "Delivering with SMS...");
@ -118,20 +119,25 @@ public class UniversalTransport {
String destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString());
if (isPushTransport(destination)) {
boolean isSmsFallbackSupported = isSmsFallbackSupported(destination);
try {
Log.w("UniversalTransport", "Delivering media message with GCM...");
pushTransport.deliver(mediaMessage, threadId);
return new MmsSendResult("push".getBytes("UTF-8"), 0, true);
} catch (IOException ioe) {
Log.w("UniversalTransport", ioe);
return mmsTransport.deliver(mediaMessage);
if (isSmsFallbackSupported) return mmsTransport.deliver(mediaMessage);
else throw new RetryLaterException(ioe);
} catch (RecipientFormattingException e) {
Log.w("UniversalTransport", e);
return mmsTransport.deliver(mediaMessage);
if (isSmsFallbackSupported) return mmsTransport.deliver(mediaMessage);
else throw new UndeliverableMessageException(e);
} catch (EncapsulatedExceptions ee) {
Log.w("UniversalTransport", ee);
if (!ee.getUnregisteredUserExceptions().isEmpty()) {
return mmsTransport.deliver(mediaMessage);
if (isSmsFallbackSupported) return mmsTransport.deliver(mediaMessage);
else throw new UndeliverableMessageException(ee);
} else {
throw new UntrustedIdentityException(ee.getUntrustedIdentityExceptions().get(0));
}
@ -203,6 +209,15 @@ public class UniversalTransport {
return recipientCount > 1;
}
private boolean isSmsFallbackSupported(String destination) {
if (GroupUtil.isEncodedGroup(destination)) {
return false;
}
Directory directory = Directory.getInstance(context);
return directory.isSmsFallbackSupported(destination);
}
private boolean isPushTransport(String destination) {
if (GroupUtil.isEncodedGroup(destination)) {
return true;
@ -215,12 +230,19 @@ public class UniversalTransport {
} catch (NotInDirectoryException e) {
try {
PushServiceSocket socket = PushServiceSocketFactory.create(context);
ContactTokenDetails registeredUser = socket.getContactTokenDetails(DirectoryUtil.getDirectoryServerToken(destination));
boolean registeredFound = !(registeredUser == null);
ContactNumberDetails numberDetails = new ContactNumberDetails(destination, registeredUser == null ? null : registeredUser.getRelay());
String contactToken = DirectoryUtil.getDirectoryServerToken(destination);
ContactTokenDetails registeredUser = socket.getContactTokenDetails(contactToken);
directory.setNumber(numberDetails, registeredFound);
return registeredFound;
if (registeredUser == null) {
registeredUser = new ContactTokenDetails();
registeredUser.setNumber(destination);
directory.setNumber(registeredUser, false);
return false;
} else {
registeredUser.setNumber(destination);
directory.setNumber(registeredUser, true);
return true;
}
} catch (IOException e1) {
Log.w("UniversalTransport", e1);
return false;

View File

@ -1,10 +1,10 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.push.ContactNumberDetails;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.DirectoryUtil;
@ -24,20 +24,19 @@ public class DirectoryHelper {
}
public static void refreshDirectory(final Context context, final PushServiceSocket socket, final String localNumber) {
final Directory directory = Directory.getInstance(context);
Directory directory = Directory.getInstance(context);
Set<String> eligibleContactNumbers = directory.getPushEligibleContactNumbers(localNumber);
Map<String, String> tokenMap = DirectoryUtil.getDirectoryServerTokenMap(eligibleContactNumbers);
List<ContactTokenDetails> activeTokens = socket.retrieveDirectory(tokenMap.keySet());
final Set<String> eligibleContactNumbers = directory.getPushEligibleContactNumbers(localNumber);
final Map<String, String> tokenMap = DirectoryUtil.getDirectoryServerTokenMap(eligibleContactNumbers);
final List<ContactTokenDetails> activeTokens = socket.retrieveDirectory(tokenMap.keySet());
if (activeTokens != null) {
final List<ContactNumberDetails> activeNumbers = ContactNumberDetails.fromContactTokenDetailsList(activeTokens, tokenMap);
for (ContactTokenDetails activeToken : activeTokens) {
eligibleContactNumbers.remove(tokenMap.get(activeToken.getToken()));
activeToken.setNumber(tokenMap.get(activeToken.getToken()));
}
directory.setNumbers(activeNumbers, eligibleContactNumbers);
directory.setNumbers(activeTokens, eligibleContactNumbers);
}
}
}