remove some unsued sms code

This commit is contained in:
B. Petersen
2018-10-08 00:13:12 +02:00
parent cbf40049a1
commit 93f781b8dc
29 changed files with 4 additions and 3180 deletions
-21
View File
@@ -400,27 +400,6 @@
</intent-filter>
</receiver>
<receiver android:name=".service.SmsDeliveryListener"
android:exported="true">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.services.MESSAGE_SENT"/>
</intent-filter>
</receiver>
<receiver android:name=".service.MmsListener"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BROADCAST_WAP_PUSH">
<intent-filter android:priority="1001">
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED"/>
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
<intent-filter>
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
<receiver android:name=".notifications.MarkReadReceiver"
android:enabled="true"
android:exported="false">
@@ -1,65 +0,0 @@
package org.thoughtcrime.securesms.backup;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.whispersystems.libsignal.util.ByteUtil;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public abstract class FullBackupBase {
@SuppressWarnings("unused")
private static final String TAG = FullBackupBase.class.getSimpleName();
static class BackupStream {
static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
try {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] input = passphrase.replace(" ", "").getBytes();
byte[] hash = input;
if (salt != null) digest.update(salt);
for (int i=0;i<250000;i++) {
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
digest.update(hash);
hash = digest.digest(input);
}
return ByteUtil.trim(hash, 32);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}
public static class BackupEvent {
public enum Type {
PROGRESS,
FINISHED
}
private final Type type;
private final int count;
BackupEvent(Type type, int count) {
this.type = type;
this.count = count;
}
public Type getType() {
return type;
}
public int getCount() {
return count;
}
}
}
@@ -1,384 +0,0 @@
package org.thoughtcrime.securesms.backup;
import android.content.Context;
import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.annimon.stream.function.Consumer;
import com.annimon.stream.function.Predicate;
import com.google.protobuf.ByteString;
import net.sqlcipher.database.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedList;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class FullBackupExporter extends FullBackupBase {
@SuppressWarnings("unused")
private static final String TAG = FullBackupExporter.class.getSimpleName();
public static void export(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase input,
@NonNull File output,
@NonNull String passphrase)
throws IOException
{
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase);
outputStream.writeDatabaseVersion(input.getVersion());
List<String> tables = exportSchema(input, outputStream);
int count = 0;
for (String table : tables) {
if (table.equals(SmsDatabase.TABLE_NAME) || table.equals(MmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0, null, count);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), cursor -> exportAttachment(attachmentSecret, cursor, outputStream), count);
} else if (!table.equals(SignedPreKeyDatabase.TABLE_NAME) &&
!table.equals(OneTimePreKeyDatabase.TABLE_NAME) &&
!table.equals(SessionDatabase.TABLE_NAME) &&
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME))
{
count = exportTable(table, input, outputStream, null, null, count);
}
}
for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
outputStream.write(preference);
}
for (File avatar : AvatarHelper.getAvatarFiles(context)) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length());
}
outputStream.writeEnd();
outputStream.close();
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count));
}
private static List<String> exportSchema(@NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream)
throws IOException
{
List<String> tables = new LinkedList<>();
try (Cursor cursor = input.rawQuery("SELECT sql, name, type FROM sqlite_master", null)) {
while (cursor != null && cursor.moveToNext()) {
String sql = cursor.getString(0);
String name = cursor.getString(1);
String type = cursor.getString(2);
if (sql != null) {
boolean isSmsFtsSecretTable = name != null && !name.equals(SearchDatabase.SMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME);
boolean isMmsFtsSecretTable = name != null && !name.equals(SearchDatabase.MMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME);
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
if ("table".equals(type)) {
tables.add(name);
}
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(cursor.getString(0)).build());
}
}
}
}
return tables;
}
private static int exportTable(@NonNull String table,
@NonNull SQLiteDatabase input,
@NonNull BackupFrameOutputStream outputStream,
@Nullable Predicate<Cursor> predicate,
@Nullable Consumer<Cursor> postProcess,
int count)
throws IOException
{
String template = "INSERT INTO " + table + " VALUES ";
try (Cursor cursor = input.rawQuery("SELECT * FROM " + table, null)) {
while (cursor != null && cursor.moveToNext()) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
if (predicate == null || predicate.test(cursor)) {
StringBuilder statement = new StringBuilder(template);
BackupProtos.SqlStatement.Builder statementBuilder = BackupProtos.SqlStatement.newBuilder();
statement.append('(');
for (int i=0;i<cursor.getColumnCount();i++) {
statement.append('?');
if (cursor.getType(i) == Cursor.FIELD_TYPE_STRING) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setStringParamter(cursor.getString(i)));
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_FLOAT) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setDoubleParameter(cursor.getDouble(i)));
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_INTEGER) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setIntegerParameter(cursor.getLong(i)));
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))));
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_NULL) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setNullparameter(true));
} else {
throw new AssertionError("unknown type?" + cursor.getType(i));
}
if (i < cursor.getColumnCount()-1) {
statement.append(',');
}
}
statement.append(')');
outputStream.write(statementBuilder.setStatement(statement.toString()).build());
if (postProcess != null) postProcess.accept(cursor);
}
}
}
return count;
}
private static void exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream) {
try {
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE));
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM));
if (!TextUtils.isEmpty(data) && size <= 0) {
size = calculateVeryOldStreamLength(attachmentSecret, random, data);
}
if (!TextUtils.isEmpty(data) && size > 0) {
InputStream inputStream;
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
private static long calculateVeryOldStreamLength(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException {
long result = 0;
InputStream inputStream;
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
int read;
byte[] buffer = new byte[8192];
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
result += read;
}
return result;
}
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MmsDatabase.EXPIRES_IN };
String where = MmsDatabase.ID + " = ?";
String[] args = new String[] { String.valueOf(mmsId) };
try (Cursor mmsCursor = db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return mmsCursor.getLong(0) == 0;
}
}
return false;
}
private static class BackupFrameOutputStream extends BackupStream {
private final OutputStream outputStream;
private final Cipher cipher;
private final Mac mac;
private final byte[] cipherKey;
private final byte[] macKey;
private byte[] iv;
private int counter;
private BackupFrameOutputStream(@NonNull File output, @NonNull String passphrase) throws IOException {
try {
byte[] salt = Util.getSecretBytes(32);
byte[] key = getBackupKey(passphrase, salt);
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64);
byte[][] split = ByteUtil.split(derived, 32, 32);
this.cipherKey = split[0];
this.macKey = split[1];
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
this.mac = Mac.getInstance("HmacSHA256");
this.outputStream = new FileOutputStream(output);
this.iv = Util.getSecretBytes(16);
this.counter = Conversions.byteArrayToInt(iv);
mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
byte[] header = BackupProtos.BackupFrame.newBuilder().setHeader(BackupProtos.Header.newBuilder()
.setIv(ByteString.copyFrom(iv))
.setSalt(ByteString.copyFrom(salt)))
.build().toByteArray();
outputStream.write(Conversions.intToByteArray(header.length));
outputStream.write(header);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public void write(BackupProtos.SharedPreference preference) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setPreference(preference).build());
}
public void write(BackupProtos.SqlStatement statement) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setStatement(statement).build());
}
public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAvatar(BackupProtos.Avatar.newBuilder()
.setName(avatarName)
.setLength(Util.toIntExact(size))
.build())
.build());
writeStream(in);
}
public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAttachment(BackupProtos.Attachment.newBuilder()
.setRowId(attachmentId.getRowId())
.setAttachmentId(attachmentId.getUniqueId())
.setLength(Util.toIntExact(size))
.build())
.build());
writeStream(in);
}
void writeDatabaseVersion(int version) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setVersion(BackupProtos.DatabaseVersion.newBuilder().setVersion(version))
.build());
}
void writeEnd() throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setEnd(true).build());
}
private void writeStream(@NonNull InputStream inputStream) throws IOException {
try {
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
mac.update(iv);
byte[] buffer = new byte[8192];
int read;
while ((read = inputStream.read(buffer)) != -1) {
byte[] ciphertext = cipher.update(buffer, 0, read);
if (ciphertext != null) {
outputStream.write(ciphertext);
mac.update(ciphertext);
}
}
byte[] remainder = cipher.doFinal();
outputStream.write(remainder);
mac.update(remainder);
byte[] attachmentDigest = mac.doFinal();
outputStream.write(attachmentDigest, 0, 10);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
private void write(@NonNull OutputStream out, @NonNull BackupProtos.BackupFrame frame) throws IOException {
try {
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
byte[] frameCiphertext = cipher.doFinal(frame.toByteArray());
byte[] frameMac = mac.doFinal(frameCiphertext);
byte[] length = Conversions.intToByteArray(frameCiphertext.length + 10);
out.write(length);
out.write(frameCiphertext);
out.write(frameMac, 0, 10);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
public void close() throws IOException {
outputStream.close();
}
}
}
@@ -1,174 +0,0 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.content.res.AssetManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.text.TextUtils;
import android.util.Log;
import org.thoughtcrime.securesms.mms.LegacyMmsConnection.Apn;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Database to query APN and MMSC information
*/
public class ApnDatabase {
private static final String TAG = ApnDatabase.class.getSimpleName();
private final SQLiteDatabase db;
private final Context context;
private static final String DATABASE_NAME = "apns.db";
private static final String ASSET_PATH = "databases" + File.separator + DATABASE_NAME;
private static final String TABLE_NAME = "apns";
private static final String ID_COLUMN = "_id";
private static final String MCC_MNC_COLUMN = "mccmnc";
private static final String MCC_COLUMN = "mcc";
private static final String MNC_COLUMN = "mnc";
private static final String CARRIER_COLUMN = "carrier";
private static final String APN_COLUMN = "apn";
private static final String MMSC_COLUMN = "mmsc";
private static final String PORT_COLUMN = "port";
private static final String TYPE_COLUMN = "type";
private static final String PROTOCOL_COLUMN = "protocol";
private static final String BEARER_COLUMN = "bearer";
private static final String ROAMING_PROTOCOL_COLUMN = "roaming_protocol";
private static final String CARRIER_ENABLED_COLUMN = "carrier_enabled";
private static final String MMS_PROXY_COLUMN = "mmsproxy";
private static final String MMS_PORT_COLUMN = "mmsport";
private static final String PROXY_COLUMN = "proxy";
private static final String MVNO_MATCH_DATA_COLUMN = "mvno_match_data";
private static final String MVNO_TYPE_COLUMN = "mvno";
private static final String AUTH_TYPE_COLUMN = "authtype";
private static final String USER_COLUMN = "user";
private static final String PASSWORD_COLUMN = "password";
private static final String SERVER_COLUMN = "server";
private static final String BASE_SELECTION = MCC_MNC_COLUMN + " = ?";
private static ApnDatabase instance = null;
public synchronized static ApnDatabase getInstance(Context context) throws IOException {
if (instance == null) instance = new ApnDatabase(context.getApplicationContext());
return instance;
}
private ApnDatabase(final Context context) throws IOException {
this.context = context;
File dbFile = context.getDatabasePath(DATABASE_NAME);
if (!dbFile.getParentFile().exists() && !dbFile.getParentFile().mkdir()) {
throw new IOException("couldn't make databases directory");
}
Util.copy(context.getAssets().open(ASSET_PATH, AssetManager.ACCESS_STREAMING),
new FileOutputStream(dbFile));
try {
this.db = SQLiteDatabase.openDatabase(context.getDatabasePath(DATABASE_NAME).getPath(),
null,
SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
} catch (SQLiteException e) {
throw new IOException(e);
}
}
private Apn getCustomApnParameters() {
String mmsc = TextSecurePreferences.getMmscUrl(context).trim();
if (!TextUtils.isEmpty(mmsc) && !mmsc.startsWith("http"))
mmsc = "http://" + mmsc;
String proxy = TextSecurePreferences.getMmscProxy(context);
String port = TextSecurePreferences.getMmscProxyPort(context);
String user = TextSecurePreferences.getMmscUsername(context);
String pass = TextSecurePreferences.getMmscPassword(context);
return new Apn(mmsc, proxy, port, user, pass);
}
public Apn getDefaultApnParameters(String mccmnc, String apn) {
if (mccmnc == null) {
Log.w(TAG, "mccmnc was null, returning null");
return Apn.EMPTY;
}
Cursor cursor = null;
try {
if (apn != null) {
Log.w(TAG, "Querying table for MCC+MNC " + mccmnc + " and APN name " + apn);
cursor = db.query(TABLE_NAME, null,
BASE_SELECTION + " AND " + APN_COLUMN + " = ?",
new String[] {mccmnc, apn},
null, null, null);
}
if (cursor == null || !cursor.moveToFirst()) {
if (cursor != null) cursor.close();
Log.w(TAG, "Querying table for MCC+MNC " + mccmnc + " without APN name");
cursor = db.query(TABLE_NAME, null,
BASE_SELECTION,
new String[] {mccmnc},
null, null, null);
}
if (cursor != null && cursor.moveToFirst()) {
Apn params = new Apn(cursor.getString(cursor.getColumnIndexOrThrow(MMSC_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(MMS_PROXY_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(MMS_PORT_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(USER_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD_COLUMN)));
Log.w(TAG, "Returning preferred APN " + params);
return params;
}
Log.w(TAG, "No matching APNs found, returning null");
return Apn.EMPTY;
} finally {
if (cursor != null) cursor.close();
}
}
public Optional<Apn> getMmsConnectionParameters(String mccmnc, String apn) {
Apn customApn = getCustomApnParameters();
Apn defaultApn = getDefaultApnParameters(mccmnc, apn);
Apn result = new Apn(customApn, defaultApn,
TextSecurePreferences.getUseCustomMmsc(context),
TextSecurePreferences.getUseCustomMmscProxy(context),
TextSecurePreferences.getUseCustomMmscProxyPort(context),
TextSecurePreferences.getUseCustomMmscUsername(context),
TextSecurePreferences.getUseCustomMmscPassword(context));
if (TextUtils.isEmpty(result.getMmsc())) return Optional.absent();
else return Optional.of(result);
}
}
@@ -1,252 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.mms.pdu_alt.CharacterSets;
import com.google.android.mms.pdu_alt.EncodedStringValue;
import com.google.android.mms.pdu_alt.PduBody;
import com.google.android.mms.pdu_alt.PduPart;
import com.google.android.mms.pdu_alt.RetrieveConf;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.MmsRadioException;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.providers.SingleUseBlobProvider;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.DuplicateMessageException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class MmsDownloadJob extends MasterSecretJob {
private static final long serialVersionUID = 1L;
private static final String TAG = MmsDownloadJob.class.getSimpleName();
private final long messageId;
private final long threadId;
private final boolean automatic;
public MmsDownloadJob(Context context, long messageId, long threadId, boolean automatic) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.withRequirement(new NetworkRequirement(context))
.withGroupId("mms-operation")
.withWakeLock(true, 30, TimeUnit.SECONDS)
.create());
this.messageId = messageId;
this.threadId = threadId;
this.automatic = automatic;
}
@Override
public void onAdded() {
if (automatic && KeyCachingService.getMasterSecret(context) == null) {
DatabaseFactory.getMmsDatabase(context).markIncomingNotificationReceived(threadId);
MessageNotifier.updateNotification(context);
}
}
@Override
public void onRun(MasterSecret masterSecret) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Optional<MmsDatabase.MmsNotificationInfo> notification = database.getNotification(messageId);
if (!notification.isPresent()) {
Log.w(TAG, "No notification for ID: " + messageId);
return;
}
try {
if (notification.get().getContentLocation() == null) {
throw new MmsException("Notification content location was null.");
}
if (!TextSecurePreferences.isPushRegistered(context)) {
throw new MmsException("Not registered");
}
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_CONNECTING);
String contentLocation = notification.get().getContentLocation();
byte[] transactionId = new byte[0];
try {
if (notification.get().getTransactionId() != null) {
transactionId = notification.get().getTransactionId().getBytes(CharacterSets.MIMENAME_ISO_8859_1);
} else {
Log.w(TAG, "No transaction ID!");
}
} catch (UnsupportedEncodingException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Downloading mms at " + Uri.parse(contentLocation).getHost() + ", subscription ID: " + notification.get().getSubscriptionId());
RetrieveConf retrieveConf = new CompatMmsConnection(context).retrieve(contentLocation, transactionId, notification.get().getSubscriptionId());
if (retrieveConf == null) {
throw new MmsException("RetrieveConf was null");
}
storeRetrievedMms(contentLocation, messageId, threadId, retrieveConf, notification.get().getSubscriptionId(), notification.get().getFrom());
} catch (ApnUnavailableException e) {
Log.w(TAG, e);
handleDownloadError(messageId, threadId, MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE,
automatic);
} catch (MmsException e) {
Log.w(TAG, e);
handleDownloadError(messageId, threadId,
MmsDatabase.Status.DOWNLOAD_HARD_FAILURE,
automatic);
} catch (MmsRadioException | IOException e) {
Log.w(TAG, e);
handleDownloadError(messageId, threadId,
MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
automatic);
} catch (DuplicateMessageException e) {
Log.w(TAG, e);
database.markAsDecryptDuplicate(messageId, threadId);
} catch (LegacyMessageException e) {
Log.w(TAG, e);
database.markAsLegacyVersion(messageId, threadId);
} catch (NoSessionException e) {
Log.w(TAG, e);
database.markAsNoSession(messageId, threadId);
} catch (InvalidMessageException e) {
Log.w(TAG, e);
database.markAsDecryptFailed(messageId, threadId);
}
}
@Override
public void onCanceled() {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE);
if (automatic) {
database.markIncomingNotificationReceived(threadId);
MessageNotifier.updateNotification(context, threadId);
}
}
@Override
public boolean onShouldRetryThrowable(Exception exception) {
return false;
}
private void storeRetrievedMms(String contentLocation,
long messageId, long threadId, RetrieveConf retrieved,
int subscriptionId, @Nullable Address notificationFrom)
throws MmsException, NoSessionException, DuplicateMessageException, InvalidMessageException,
LegacyMessageException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
SingleUseBlobProvider provider = SingleUseBlobProvider.getInstance();
Optional<Address> group = Optional.absent();
Set<Address> members = new HashSet<>();
String body = null;
List<Attachment> attachments = new LinkedList<>();
Address from;
if (retrieved.getFrom() != null) {
from = Address.fromExternal(context, Util.toIsoString(retrieved.getFrom().getTextString()));
} else if (notificationFrom != null) {
from = notificationFrom;
} else {
from = Address.UNKNOWN;
}
if (retrieved.getTo() != null) {
for (EncodedStringValue toValue : retrieved.getTo()) {
members.add(Address.fromExternal(context, Util.toIsoString(toValue.getTextString())));
}
}
if (retrieved.getCc() != null) {
for (EncodedStringValue ccValue : retrieved.getCc()) {
members.add(Address.fromExternal(context, Util.toIsoString(ccValue.getTextString())));
}
}
members.add(from);
members.add(Address.fromExternal(context, TextSecurePreferences.getLocalNumber(context)));
if (retrieved.getBody() != null) {
body = PartParser.getMessageText(retrieved.getBody());
PduBody media = PartParser.getSupportedMediaParts(retrieved.getBody());
for (int i=0;i<media.getPartsNum();i++) {
PduPart part = media.getPart(i);
if (part.getData() != null) {
Uri uri = provider.createUri(part.getData());
String name = null;
if (part.getName() != null) name = Util.toIsoString(part.getName());
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
part.getData().length, name, false, false));
}
}
}
if (members.size() > 2) {
group = Optional.of(Address.fromSerialized(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(new LinkedList<>(members), true)));
}
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false);
Optional<InsertResult> insertResult = database.insertMessageInbox(message, contentLocation, threadId);
if (insertResult.isPresent()) {
database.delete(messageId);
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
}
}
private void handleDownloadError(long messageId, long threadId, int downloadStatus, boolean automatic)
{
MmsDatabase db = DatabaseFactory.getMmsDatabase(context);
db.markDownloadState(messageId, downloadStatus);
if (automatic) {
db.markIncomingNotificationReceived(threadId);
MessageNotifier.updateNotification(context, threadId);
}
}
}
@@ -1,98 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import com.google.android.mms.pdu_alt.GenericPdu;
import com.google.android.mms.pdu_alt.NotificationInd;
import com.google.android.mms.pdu_alt.PduHeaders;
import com.google.android.mms.pdu_alt.PduParser;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
public class MmsReceiveJob extends ContextJob {
private static final long serialVersionUID = 1L;
private static final String TAG = MmsReceiveJob.class.getSimpleName();
private final byte[] data;
private final int subscriptionId;
public MmsReceiveJob(Context context, byte[] data, int subscriptionId) {
super(context, JobParameters.newBuilder()
.withWakeLock(true)
.withPersistence().create());
this.data = data;
this.subscriptionId = subscriptionId;
}
@Override
public void onAdded() {
}
@Override
public void onRun() {
if (data == null) {
Log.w(TAG, "Received NULL pdu, ignoring...");
return;
}
PduParser parser = new PduParser(data);
GenericPdu pdu = null;
try {
pdu = parser.parse();
} catch (RuntimeException e) {
Log.w(TAG, e);
}
if (isNotification(pdu) && !isBlocked(pdu)) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox((NotificationInd)pdu, subscriptionId);
Log.w(TAG, "Inserted received MMS notification...");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MmsDownloadJob(context,
messageAndThreadId.first,
messageAndThreadId.second,
true));
} else if (isNotification(pdu)) {
Log.w(TAG, "*** Received blocked MMS, ignoring...");
}
}
@Override
public void onCanceled() {
// TODO
}
@Override
public boolean onShouldRetry(Exception exception) {
return false;
}
private boolean isBlocked(GenericPdu pdu) {
if (pdu.getFrom() != null && pdu.getFrom().getTextString() != null) {
Recipient recipients = Recipient.from(context, Address.fromExternal(context, Util.toIsoString(pdu.getFrom().getTextString())), false);
return recipients.isBlocked();
}
return false;
}
private boolean isNotification(GenericPdu pdu) {
return pdu != null && pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
}
}
@@ -1,298 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
import com.android.mms.dom.smil.parser.SmilXmlSerializer;
import com.google.android.mms.ContentType;
import com.google.android.mms.InvalidHeaderValueException;
import com.google.android.mms.pdu_alt.CharacterSets;
import com.google.android.mms.pdu_alt.EncodedStringValue;
import com.google.android.mms.pdu_alt.PduBody;
import com.google.android.mms.pdu_alt.PduComposer;
import com.google.android.mms.pdu_alt.PduHeaders;
import com.google.android.mms.pdu_alt.PduPart;
import com.google.android.mms.pdu_alt.SendConf;
import com.google.android.mms.pdu_alt.SendReq;
import com.google.android.mms.smil.SmilHelper;
import com.klinker.android.send_message.Utils;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class MmsSendJob extends SendJob {
private static final long serialVersionUID = 0L;
private static final String TAG = MmsSendJob.class.getSimpleName();
private final long messageId;
public MmsSendJob(Context context, long messageId) {
super(context, JobParameters.newBuilder()
.withGroupId("mms-operation")
.withRequirement(new NetworkRequirement(context))
.withRequirement(new MasterSecretRequirement(context))
.withPersistence()
.create());
this.messageId = messageId;
}
@Override
public void onAdded() {
}
@Override
public void onSend(MasterSecret masterSecret) throws MmsException, NoSuchMessageException, IOException {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
try {
SendReq pdu = constructSendPdu(message);
validateDestinations(message, pdu);
final byte[] pduBytes = getPduBytes(pdu);
final SendConf sendConf = new CompatMmsConnection(context).send(pduBytes, message.getSubscriptionId());
final MmsSendResult result = getSendResult(sendConf, pdu);
database.markAsSent(messageId, false);
markAttachmentsUploaded(messageId, message.getAttachments());
} catch (UndeliverableMessageException | IOException e) {
Log.w(TAG, e);
database.markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
} catch (InsecureFallbackApprovalException e) {
Log.w(TAG, e);
database.markAsPendingInsecureSmsFallback(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
}
@Override
public boolean onShouldRetryThrowable(Exception exception) {
return false;
}
@Override
public void onCanceled() {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
private byte[] getPduBytes(SendReq message)
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
{
byte[] pduBytes = new PduComposer(context, message).make();
if (pduBytes == null) {
throw new UndeliverableMessageException("PDU composition failed, null payload");
}
return pduBytes;
}
private MmsSendResult getSendResult(SendConf conf, SendReq message)
throws UndeliverableMessageException
{
if (conf == null) {
throw new UndeliverableMessageException("No M-Send.conf received in response to send.");
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
throw new UndeliverableMessageException("Got bad response: " + conf.getResponseStatus());
} else if (isInconsistentResponse(message, conf)) {
throw new UndeliverableMessageException("Mismatched response!");
} else {
return new MmsSendResult(conf.getMessageId(), conf.getResponseStatus());
}
}
private boolean isInconsistentResponse(SendReq message, SendConf response) {
Log.w(TAG, "Comparing: " + Hex.toString(message.getTransactionId()));
Log.w(TAG, "With: " + Hex.toString(response.getTransactionId()));
return !Arrays.equals(message.getTransactionId(), response.getTransactionId());
}
private void validateDestinations(EncodedStringValue[] destinations) throws UndeliverableMessageException {
if (destinations == null) return;
for (EncodedStringValue destination : destinations) {
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
throw new UndeliverableMessageException("Invalid destination: " +
(destination == null ? null : destination.getString()));
}
}
}
private void validateDestinations(OutgoingMediaMessage media, SendReq message) throws UndeliverableMessageException {
validateDestinations(message.getTo());
validateDestinations(message.getCc());
validateDestinations(message.getBcc());
if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) {
throw new UndeliverableMessageException("No to, cc, or bcc specified!");
}
if (media.isSecure()) {
throw new UndeliverableMessageException("Attempt to send encrypted MMS?");
}
}
private SendReq constructSendPdu(OutgoingMediaMessage message)
throws UndeliverableMessageException
{
SendReq req = new SendReq();
String lineNumber = getMyNumber(context);
Address destination = message.getRecipient().getAddress();
MediaConstraints mediaConstraints = MediaConstraints.getMmsMediaConstraints(message.getSubscriptionId());
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
if (!TextUtils.isEmpty(lineNumber)) {
req.setFrom(new EncodedStringValue(lineNumber));
} else {
req.setFrom(new EncodedStringValue(TextSecurePreferences.getLocalNumber(context)));
}
if (destination.isMmsGroup()) {
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(destination.toGroupString(), false);
for (Recipient member : members) {
if (message.getDistributionType() == ThreadDatabase.DistributionTypes.BROADCAST) {
req.addBcc(new EncodedStringValue(member.getAddress().serialize()));
} else {
req.addTo(new EncodedStringValue(member.getAddress().serialize()));
}
}
} else {
req.addTo(new EncodedStringValue(destination.serialize()));
}
req.setDate(System.currentTimeMillis() / 1000);
PduBody body = new PduBody();
int size = 0;
if (!TextUtils.isEmpty(message.getBody())) {
PduPart part = new PduPart();
String name = String.valueOf(System.currentTimeMillis());
part.setData(Util.toUtf8Bytes(message.getBody()));
part.setCharset(CharacterSets.UTF_8);
part.setContentType(ContentType.TEXT_PLAIN.getBytes());
part.setContentId(name.getBytes());
part.setContentLocation((name + ".txt").getBytes());
part.setName((name + ".txt").getBytes());
body.addPart(part);
size += getPartSize(part);
}
for (Attachment attachment : scaledAttachments) {
try {
if (attachment.getDataUri() == null) throw new IOException("Assertion failed, attachment for outgoing MMS has no data!");
String fileName = attachment.getFileName();
PduPart part = new PduPart();
if (fileName == null) {
fileName = String.valueOf(Math.abs(Util.getSecureRandom().nextLong()));
String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.getContentType());
if (fileExtension != null) fileName = fileName + "." + fileExtension;
}
if (attachment.getContentType().startsWith("text")) {
part.setCharset(CharacterSets.UTF_8);
}
part.setContentType(attachment.getContentType().getBytes());
part.setContentLocation(fileName.getBytes());
part.setName(fileName.getBytes());
int index = fileName.lastIndexOf(".");
String contentId = (index == -1) ? fileName : fileName.substring(0, index);
part.setContentId(contentId.getBytes());
part.setData(Util.readFully(PartAuthority.getAttachmentStream(context, attachment.getDataUri())));
body.addPart(part);
size += getPartSize(part);
} catch (IOException e) {
Log.w(TAG, e);
}
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
SmilXmlSerializer.serialize(SmilHelper.createSmilDocument(body), out);
PduPart smilPart = new PduPart();
smilPart.setContentId("smil".getBytes());
smilPart.setContentLocation("smil.xml".getBytes());
smilPart.setContentType(ContentType.APP_SMIL.getBytes());
smilPart.setData(out.toByteArray());
body.addPart(0, smilPart);
req.setBody(body);
req.setMessageSize(size);
req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes());
req.setExpiry(7 * 24 * 60 * 60);
try {
req.setPriority(PduHeaders.PRIORITY_NORMAL);
req.setDeliveryReport(PduHeaders.VALUE_NO);
req.setReadReport(PduHeaders.VALUE_NO);
} catch (InvalidHeaderValueException e) {}
return req;
}
private long getPartSize(PduPart part) {
return part.getName().length + part.getContentLocation().length +
part.getContentType().length + part.getData().length +
part.getContentId().length;
}
private void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
if (recipient != null) {
MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
}
}
private String getMyNumber(Context context) throws UndeliverableMessageException {
try {
return Utils.getMyPhoneNumber(context);
} catch (SecurityException e) {
throw new UndeliverableMessageException(e);
}
}
}
@@ -1,208 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsManager;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.jobs.requirements.NetworkOrServiceRequirement;
import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirement;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import java.util.ArrayList;
public class SmsSendJob extends SendJob {
private static final long serialVersionUID = -5118520036244759718L;
private static final String TAG = SmsSendJob.class.getSimpleName();
private final long messageId;
public SmsSendJob(Context context, long messageId, String name) {
super(context, constructParameters(context, name));
this.messageId = messageId;
}
@Override
public void onAdded() {}
@Override
public void onSend(MasterSecret masterSecret) throws NoSuchMessageException {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
SmsMessageRecord record = database.getMessage(messageId);
try {
Log.w(TAG, "Sending message: " + messageId);
deliver(record);
} catch (UndeliverableMessageException ude) {
Log.w(TAG, ude);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId());
}
}
@Override
public boolean onShouldRetryThrowable(Exception throwable) {
return false;
}
@Override
public void onCanceled() {
Log.w(TAG, "onCanceled()");
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
}
private void deliver(SmsMessageRecord message)
throws UndeliverableMessageException
{
if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) {
throw new UndeliverableMessageException("Trying to send a secure SMS?");
}
String recipient = message.getIndividualRecipient().getAddress().serialize();
// See issue #1516 for bug report, and discussion on commits related to #4833 for problems
// related to the original fix to #1516. This still may not be a correct fix if networks allow
// SMS/MMS sending to alphanumeric recipients other than email addresses, but should also
// help to fix issue #3099.
if (!NumberUtil.isValidEmail(recipient)) {
recipient = PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(recipient));
}
if (!NumberUtil.isValidSmsOrEmail(recipient)) {
throw new UndeliverableMessageException("Not a valid SMS destination! " + recipient);
}
ArrayList<String> messages = SmsManager.getDefault().divideMessage(message.getBody());
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false);
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
// NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients
// and messages, this will throw an NPE. We have no idea why, so we're just
// catching it and marking the message as a failure. That way at least it doesn't
// repeatedly crash every time you start the app.
try {
getSmsManagerFor(message.getSubscriptionId()).sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
} catch (NullPointerException | IllegalArgumentException npe) {
Log.w(TAG, npe);
Log.w(TAG, "Recipient: " + recipient);
Log.w(TAG, "Message Parts: " + messages.size());
try {
for (int i=0;i<messages.size();i++) {
getSmsManagerFor(message.getSubscriptionId()).sendTextMessage(recipient, null, messages.get(i),
sentIntents.get(i),
deliveredIntents == null ? null : deliveredIntents.get(i));
}
} catch (NullPointerException | IllegalArgumentException npe2) {
Log.w(TAG, npe);
throw new UndeliverableMessageException(npe2);
}
} catch (SecurityException se) {
Log.w(TAG, se);
throw new UndeliverableMessageException(se);
}
}
private ArrayList<PendingIntent> constructSentIntents(long messageId, long type,
ArrayList<String> messages, boolean secure)
{
ArrayList<PendingIntent> sentIntents = new ArrayList<>(messages.size());
for (String ignored : messages) {
sentIntents.add(PendingIntent.getBroadcast(context, 0,
constructSentIntent(context, messageId, type, secure, false),
0));
}
return sentIntents;
}
private ArrayList<PendingIntent> constructDeliveredIntents(long messageId, long type, ArrayList<String> messages) {
if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) {
return null;
}
ArrayList<PendingIntent> deliveredIntents = new ArrayList<>(messages.size());
for (String ignored : messages) {
deliveredIntents.add(PendingIntent.getBroadcast(context, 0,
constructDeliveredIntent(context, messageId, type),
0));
}
return deliveredIntents;
}
private Intent constructSentIntent(Context context, long messageId, long type,
boolean upgraded, boolean push)
{
Intent pending = new Intent(SmsDeliveryListener.SENT_SMS_ACTION,
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
context, SmsDeliveryListener.class);
pending.putExtra("type", type);
pending.putExtra("message_id", messageId);
pending.putExtra("upgraded", upgraded);
pending.putExtra("push", push);
return pending;
}
private Intent constructDeliveredIntent(Context context, long messageId, long type) {
Intent pending = new Intent(SmsDeliveryListener.DELIVERED_SMS_ACTION,
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
context, SmsDeliveryListener.class);
pending.putExtra("type", type);
pending.putExtra("message_id", messageId);
return pending;
}
private SmsManager getSmsManagerFor(int subscriptionId) {
if (Build.VERSION.SDK_INT >= 22 && subscriptionId != -1) {
return SmsManager.getSmsManagerForSubscriptionId(subscriptionId);
} else {
return SmsManager.getDefault();
}
}
private static JobParameters constructParameters(Context context, String name) {
JobParameters.Builder builder = JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.withRetryCount(15)
.withGroupId(name);
if (TextSecurePreferences.isWifiSmsEnabled(context)) {
builder.withRequirement(new NetworkOrServiceRequirement(context));
} else {
builder.withRequirement(new ServiceRequirement(context));
}
return builder.create();
}
}
@@ -1,96 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.app.Activity;
import android.content.Context;
import android.telephony.SmsManager;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
public class SmsSentJob extends MasterSecretJob {
private static final long serialVersionUID = -2624694558755317560L;
private static final String TAG = SmsSentJob.class.getSimpleName();
private final long messageId;
private final String action;
private final int result;
public SmsSentJob(Context context, long messageId, String action, int result) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.create());
this.messageId = messageId;
this.action = action;
this.result = result;
}
@Override
public void onAdded() {
}
@Override
public void onRun(MasterSecret masterSecret) {
Log.w(TAG, "Got SMS callback: " + action + " , " + result);
switch (action) {
case SmsDeliveryListener.SENT_SMS_ACTION:
handleSentResult(messageId, result);
break;
case SmsDeliveryListener.DELIVERED_SMS_ACTION:
handleDeliveredResult(messageId, result);
break;
}
}
@Override
public boolean onShouldRetryThrowable(Exception throwable) {
return false;
}
@Override
public void onCanceled() {
}
private void handleDeliveredResult(long messageId, int result) {
DatabaseFactory.getSmsDatabase(context).markStatus(messageId, result);
}
private void handleSentResult(long messageId, int result) {
try {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
SmsMessageRecord record = database.getMessage(messageId);
switch (result) {
case Activity.RESULT_OK:
database.markAsSent(messageId, false);
break;
case SmsManager.RESULT_ERROR_NO_SERVICE:
case SmsManager.RESULT_ERROR_RADIO_OFF:
Log.w(TAG, "Service connectivity problem, requeuing...");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new SmsSendJob(context, messageId, record.getIndividualRecipient().getAddress().serialize()));
break;
default:
database.markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId());
}
} catch (NoSuchMessageException e) {
Log.w(TAG, e);
}
}
}
@@ -1,101 +0,0 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.mms.pdu_alt.PduHeaders;
import com.google.android.mms.pdu_alt.RetrieveConf;
import com.google.android.mms.pdu_alt.SendConf;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import java.io.IOException;
public class CompatMmsConnection implements OutgoingMmsConnection, IncomingMmsConnection {
private static final String TAG = CompatMmsConnection.class.getSimpleName();
private Context context;
public CompatMmsConnection(Context context) {
this.context = context;
}
@Nullable
@Override
public SendConf send(@NonNull byte[] pduBytes, int subscriptionId)
throws UndeliverableMessageException
{
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
try {
Log.w(TAG, "Sending via Lollipop API");
return new OutgoingLollipopMmsConnection(context).send(pduBytes, subscriptionId);
} catch (UndeliverableMessageException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Falling back to legacy connection...");
}
if (subscriptionId == -1) {
Log.w(TAG, "Sending via legacy connection");
try {
SendConf result = new OutgoingLegacyMmsConnection(context).send(pduBytes, subscriptionId);
if (result != null && result.getResponseStatus() == PduHeaders.RESPONSE_STATUS_OK) {
return result;
} else {
Log.w(TAG, "Got bad legacy response: " + (result != null ? result.getResponseStatus() : null));
}
} catch (UndeliverableMessageException | ApnUnavailableException e) {
Log.w(TAG, e);
}
}
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP_MR1) {
Log.w(TAG, "Falling back to sending via Lollipop API");
return new OutgoingLollipopMmsConnection(context).send(pduBytes, subscriptionId);
}
throw new UndeliverableMessageException("Both lollipop and legacy connections failed...");
}
@Nullable
@Override
public RetrieveConf retrieve(@NonNull String contentLocation,
byte[] transactionId,
int subscriptionId)
throws MmsException, MmsRadioException, ApnUnavailableException, IOException
{
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
Log.w(TAG, "Receiving via Lollipop API");
try {
return new IncomingLollipopMmsConnection(context).retrieve(contentLocation, transactionId, subscriptionId);
} catch (MmsException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Falling back to receiving via legacy connection");
}
if (VERSION.SDK_INT < 22 || subscriptionId == -1) {
Log.w(TAG, "Receiving via legacy API");
try {
return new IncomingLegacyMmsConnection(context).retrieve(contentLocation, transactionId, subscriptionId);
} catch (MmsRadioException | ApnUnavailableException | IOException e) {
Log.w(TAG, e);
}
}
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP_MR1) {
Log.w(TAG, "Falling back to receiving via Lollipop API");
return new IncomingLollipopMmsConnection(context).retrieve(contentLocation, transactionId, subscriptionId);
}
throw new IOException("Both lollipop and fallback APIs failed...");
}
}
@@ -1,156 +0,0 @@
/**
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.mms.InvalidHeaderValueException;
import com.google.android.mms.pdu_alt.NotifyRespInd;
import com.google.android.mms.pdu_alt.PduComposer;
import com.google.android.mms.pdu_alt.PduHeaders;
import com.google.android.mms.pdu_alt.PduParser;
import com.google.android.mms.pdu_alt.RetrieveConf;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGetHC4;
import org.apache.http.client.methods.HttpUriRequest;
import java.io.IOException;
import java.util.Arrays;
@SuppressWarnings("deprecation")
public class IncomingLegacyMmsConnection extends LegacyMmsConnection implements IncomingMmsConnection {
private static final String TAG = IncomingLegacyMmsConnection.class.getSimpleName();
public IncomingLegacyMmsConnection(Context context) throws ApnUnavailableException {
super(context);
}
private HttpUriRequest constructRequest(Apn contentApn, boolean useProxy) throws IOException {
HttpGetHC4 request;
try {
request = new HttpGetHC4(contentApn.getMmsc());
} catch (IllegalArgumentException e) {
// #7339
throw new IOException(e);
}
for (Header header : getBaseHeaders()) {
request.addHeader(header);
}
if (useProxy) {
HttpHost proxy = new HttpHost(contentApn.getProxy(), contentApn.getPort());
request.setConfig(RequestConfig.custom().setProxy(proxy).build());
}
return request;
}
@Override
public @Nullable RetrieveConf retrieve(@NonNull String contentLocation,
byte[] transactionId, int subscriptionId)
throws MmsRadioException, ApnUnavailableException, IOException
{
MmsRadio radio = MmsRadio.getInstance(context);
Apn contentApn = new Apn(contentLocation, apn.getProxy(), Integer.toString(apn.getPort()), apn.getUsername(), apn.getPassword());
if (isDirectConnect()) {
Log.w(TAG, "Connecting directly...");
try {
return retrieve(contentApn, transactionId, false, false);
} catch (IOException | ApnUnavailableException e) {
Log.w(TAG, e);
}
}
Log.w(TAG, "Changing radio to MMS mode..");
radio.connect();
try {
Log.w(TAG, "Downloading in MMS mode with proxy...");
try {
return retrieve(contentApn, transactionId, true, true);
} catch (IOException | ApnUnavailableException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Downloading in MMS mode without proxy...");
return retrieve(contentApn, transactionId, true, false);
} finally {
radio.disconnect();
}
}
public RetrieveConf retrieve(Apn contentApn, byte[] transactionId, boolean usingMmsRadio, boolean useProxyIfAvailable)
throws IOException, ApnUnavailableException
{
byte[] pdu = null;
final boolean useProxy = useProxyIfAvailable && contentApn.hasProxy();
final String targetHost = useProxy
? contentApn.getProxy()
: Uri.parse(contentApn.getMmsc()).getHost();
if (checkRouteToHost(context, targetHost, usingMmsRadio)) {
Log.w(TAG, "got successful route to host " + targetHost);
pdu = execute(constructRequest(contentApn, useProxy));
}
if (pdu == null) {
throw new IOException("Connection manager could not obtain route to host.");
}
RetrieveConf retrieved = (RetrieveConf)new PduParser(pdu).parse();
if (retrieved == null) {
Log.w(TAG, "Couldn't parse PDU, byte response: " + Arrays.toString(pdu));
Log.w(TAG, "Couldn't parse PDU, ASCII: " + new String(pdu));
throw new IOException("Bad retrieved PDU");
}
sendRetrievedAcknowledgement(transactionId, usingMmsRadio, useProxy);
return retrieved;
}
private void sendRetrievedAcknowledgement(byte[] transactionId,
boolean usingRadio,
boolean useProxy)
throws ApnUnavailableException
{
try {
NotifyRespInd notifyResponse = new NotifyRespInd(PduHeaders.CURRENT_MMS_VERSION,
transactionId,
PduHeaders.STATUS_RETRIEVED);
OutgoingLegacyMmsConnection connection = new OutgoingLegacyMmsConnection(context);
connection.sendNotificationReceived(new PduComposer(context, notifyResponse).make(), usingRadio, useProxy);
} catch (InvalidHeaderValueException | IOException e) {
Log.w(TAG, e);
}
}
}
@@ -1,296 +0,0 @@
/**
* 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.mms;
import android.content.Context;
import android.net.ConnectivityManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import org.apache.http.Header;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.NoConnectionReuseStrategyHC4;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.thoughtcrime.securesms.database.ApnDatabase;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@SuppressWarnings("deprecation")
public abstract class LegacyMmsConnection {
public static final String USER_AGENT = "Android-Mms/2.0";
private static final String TAG = LegacyMmsConnection.class.getSimpleName();
protected final Context context;
protected final Apn apn;
protected LegacyMmsConnection(Context context) throws ApnUnavailableException {
this.context = context;
this.apn = getApn(context);
}
public static Apn getApn(Context context) throws ApnUnavailableException {
try {
Optional<Apn> params = ApnDatabase.getInstance(context)
.getMmsConnectionParameters(TelephonyUtil.getMccMnc(context),
TelephonyUtil.getApn(context));
if (!params.isPresent()) {
throw new ApnUnavailableException("No parameters available from ApnDefaults.");
}
return params.get();
} catch (IOException ioe) {
throw new ApnUnavailableException("ApnDatabase threw an IOException", ioe);
}
}
protected boolean isDirectConnect() {
// We think Sprint supports direct connection over wifi/data, but not Verizon
Set<String> sprintMccMncs = new HashSet<String>() {{
add("312530");
add("311880");
add("311870");
add("311490");
add("310120");
add("316010");
add("312190");
}};
return ServiceUtil.getTelephonyManager(context).getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA &&
sprintMccMncs.contains(TelephonyUtil.getMccMnc(context));
}
@SuppressWarnings("TryWithIdenticalCatches")
protected static boolean checkRouteToHost(Context context, String host, boolean usingMmsRadio)
throws IOException
{
InetAddress inetAddress = InetAddress.getByName(host);
if (!usingMmsRadio) {
if (inetAddress.isSiteLocalAddress()) {
throw new IOException("RFC1918 address in non-MMS radio situation!");
}
Log.w(TAG, "returning vacuous success since MMS radio is not in use");
return true;
}
if (inetAddress == null) {
throw new IOException("Unable to lookup host: InetAddress.getByName() returned null.");
}
byte[] ipAddressBytes = inetAddress.getAddress();
if (ipAddressBytes == null) {
Log.w(TAG, "resolved IP address bytes are null, returning true to attempt a connection anyway.");
return true;
}
Log.w(TAG, "Checking route to address: " + host + ", " + inetAddress.getHostAddress());
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
try {
final Method requestRouteMethod = manager.getClass().getMethod("requestRouteToHostAddress", Integer.TYPE, InetAddress.class);
final boolean routeToHostObtained = (Boolean) requestRouteMethod.invoke(manager, MmsRadio.TYPE_MOBILE_MMS, inetAddress);
Log.w(TAG, "requestRouteToHostAddress(" + inetAddress + ") -> " + routeToHostObtained);
return routeToHostObtained;
} catch (NoSuchMethodException nsme) {
Log.w(TAG, nsme);
} catch (IllegalAccessException iae) {
Log.w(TAG, iae);
} catch (InvocationTargetException ite) {
Log.w(TAG, ite);
}
return false;
}
protected static byte[] parseResponse(InputStream is) throws IOException {
InputStream in = new BufferedInputStream(is);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.copy(in, baos);
Log.w(TAG, "Received full server response, " + baos.size() + " bytes");
return baos.toByteArray();
}
protected CloseableHttpClient constructHttpClient() throws IOException {
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(20 * 1000)
.setConnectionRequestTimeout(20 * 1000)
.setSocketTimeout(20 * 1000)
.setMaxRedirects(20)
.build();
URL mmsc = new URL(apn.getMmsc());
CredentialsProvider credsProvider = new BasicCredentialsProvider();
if (apn.hasAuthentication()) {
credsProvider.setCredentials(new AuthScope(mmsc.getHost(), mmsc.getPort() > -1 ? mmsc.getPort() : mmsc.getDefaultPort()),
new UsernamePasswordCredentials(apn.getUsername(), apn.getPassword()));
}
return HttpClients.custom()
.setConnectionReuseStrategy(new NoConnectionReuseStrategyHC4())
.setRedirectStrategy(new LaxRedirectStrategy())
.setUserAgent(TextSecurePreferences.getMmsUserAgent(context, USER_AGENT))
.setConnectionManager(new BasicHttpClientConnectionManager())
.setDefaultRequestConfig(config)
.setDefaultCredentialsProvider(credsProvider)
.build();
}
protected byte[] execute(HttpUriRequest request) throws IOException {
Log.w(TAG, "connecting to " + apn.getMmsc());
CloseableHttpClient client = null;
CloseableHttpResponse response = null;
try {
client = constructHttpClient();
response = client.execute(request);
Log.w(TAG, "* response code: " + response.getStatusLine());
if (response.getStatusLine().getStatusCode() == 200) {
return parseResponse(response.getEntity().getContent());
}
} catch (NullPointerException npe) {
// TODO determine root cause
// see: https://github.com/signalapp/Signal-Android/issues/4379
throw new IOException(npe);
} finally {
if (response != null) response.close();
if (client != null) client.close();
}
throw new IOException("unhandled response code");
}
protected List<Header> getBaseHeaders() {
final String number = TelephonyUtil.getManager(context).getLine1Number(); ;
return new LinkedList<Header>() {{
add(new BasicHeader("Accept", "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"));
add(new BasicHeader("x-wap-profile", "http://www.google.com/oha/rdf/ua-profile-kila.xml"));
add(new BasicHeader("Content-Type", "application/vnd.wap.mms-message"));
add(new BasicHeader("x-carrier-magic", "http://magic.google.com"));
if (!TextUtils.isEmpty(number)) {
add(new BasicHeader("x-up-calling-line-id", number));
add(new BasicHeader("X-MDN", number));
}
}};
}
public static class Apn {
public static Apn EMPTY = new Apn("", "", "", "", "");
private final String mmsc;
private final String proxy;
private final String port;
private final String username;
private final String password;
public Apn(String mmsc, String proxy, String port, String username, String password) {
this.mmsc = mmsc;
this.proxy = proxy;
this.port = port;
this.username = username;
this.password = password;
}
public Apn(Apn customApn, Apn defaultApn,
boolean useCustomMmsc,
boolean useCustomProxy,
boolean useCustomProxyPort,
boolean useCustomUsername,
boolean useCustomPassword)
{
this.mmsc = useCustomMmsc ? customApn.mmsc : defaultApn.mmsc;
this.proxy = useCustomProxy ? customApn.proxy : defaultApn.proxy;
this.port = useCustomProxyPort ? customApn.port : defaultApn.port;
this.username = useCustomUsername ? customApn.username : defaultApn.username;
this.password = useCustomPassword ? customApn.password : defaultApn.password;
}
public boolean hasProxy() {
return !TextUtils.isEmpty(proxy);
}
public String getMmsc() {
return mmsc;
}
public String getProxy() {
return hasProxy() ? proxy : null;
}
public int getPort() {
return TextUtils.isEmpty(port) ? 80 : Integer.parseInt(port);
}
public boolean hasAuthentication() {
return !TextUtils.isEmpty(username);
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
@Override
public String toString() {
return Apn.class.getSimpleName() +
"{ mmsc: \"" + mmsc + "\"" +
", proxy: " + (proxy == null ? "none" : '"' + proxy + '"') +
", port: " + (port == null ? "(none)" : port) +
", user: " + (username == null ? "none" : '"' + username + '"') +
", pass: " + (password == null ? "none" : '"' + password + '"') + " }";
}
}
}
@@ -1,162 +0,0 @@
/**
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.mms.pdu_alt.PduParser;
import com.google.android.mms.pdu_alt.SendConf;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPostHC4;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ByteArrayEntityHC4;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import java.io.IOException;
@SuppressWarnings("deprecation")
public class OutgoingLegacyMmsConnection extends LegacyMmsConnection implements OutgoingMmsConnection {
private final static String TAG = OutgoingLegacyMmsConnection.class.getSimpleName();
public OutgoingLegacyMmsConnection(Context context) throws ApnUnavailableException {
super(context);
}
private HttpUriRequest constructRequest(byte[] pduBytes, boolean useProxy)
throws IOException
{
try {
HttpPostHC4 request = new HttpPostHC4(apn.getMmsc());
for (Header header : getBaseHeaders()) {
request.addHeader(header);
}
request.setEntity(new ByteArrayEntityHC4(pduBytes));
if (useProxy) {
HttpHost proxy = new HttpHost(apn.getProxy(), apn.getPort());
request.setConfig(RequestConfig.custom().setProxy(proxy).build());
}
return request;
} catch (IllegalArgumentException iae) {
throw new IOException(iae);
}
}
public void sendNotificationReceived(byte[] pduBytes, boolean usingMmsRadio, boolean useProxyIfAvailable)
throws IOException
{
sendBytes(pduBytes, usingMmsRadio, useProxyIfAvailable);
}
@Override
public @Nullable SendConf send(@NonNull byte[] pduBytes, int subscriptionId) throws UndeliverableMessageException {
try {
MmsRadio radio = MmsRadio.getInstance(context);
if (isDirectConnect()) {
Log.w(TAG, "Sending MMS directly without radio change...");
try {
return send(pduBytes, false, false);
} catch (IOException e) {
Log.w(TAG, e);
}
}
Log.w(TAG, "Sending MMS with radio change and proxy...");
radio.connect();
try {
try {
return send(pduBytes, true, true);
} catch (IOException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Sending MMS with radio change and without proxy...");
try {
return send(pduBytes, true, false);
} catch (IOException ioe) {
Log.w(TAG, ioe);
throw new UndeliverableMessageException(ioe);
}
} finally {
radio.disconnect();
}
} catch (MmsRadioException e) {
Log.w(TAG, e);
throw new UndeliverableMessageException(e);
}
}
private SendConf send(byte[] pduBytes, boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
byte[] response = sendBytes(pduBytes, useMmsRadio, useProxyIfAvailable);
return (SendConf) new PduParser(response).parse();
}
private byte[] sendBytes(byte[] pduBytes, boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
final boolean useProxy = useProxyIfAvailable && apn.hasProxy();
final String targetHost = useProxy
? apn.getProxy()
: Uri.parse(apn.getMmsc()).getHost();
Log.w(TAG, "Sending MMS of length: " + pduBytes.length
+ (useMmsRadio ? ", using mms radio" : "")
+ (useProxy ? ", using proxy" : ""));
try {
if (checkRouteToHost(context, targetHost, useMmsRadio)) {
Log.w(TAG, "got successful route to host " + targetHost);
byte[] response = execute(constructRequest(pduBytes, useProxy));
if (response != null) return response;
}
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
throw new IOException("Connection manager could not obtain route to host.");
}
public static boolean isConnectionPossible(Context context) {
try {
ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmsRadio.TYPE_MOBILE_MMS);
if (networkInfo == null) {
Log.w(TAG, "MMS network info was null, unsupported by this device");
return false;
}
getApn(context);
return true;
} catch (ApnUnavailableException e) {
Log.w(TAG, e);
return false;
}
}
}
@@ -1,78 +0,0 @@
/**
* 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.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.provider.Telephony;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.jobs.MmsReceiveJob;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
public class MmsListener extends BroadcastReceiver {
private static final String TAG = MmsListener.class.getSimpleName();
private boolean isRelevant(Context context, Intent intent) {
if (!ApplicationMigrationService.isDatabaseImported(context)) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION.equals(intent.getAction()) &&
Util.isDefaultSmsProvider(context))
{
return false;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT &&
TextSecurePreferences.isInterceptAllMmsEnabled(context))
{
return true;
}
return false;
}
@Override
public void onReceive(Context context, Intent intent) {
Log.w(TAG, "Got MMS broadcast..." + intent.getAction());
if ((Telephony.Sms.Intents.WAP_PUSH_DELIVER_ACTION.equals(intent.getAction()) &&
Util.isDefaultSmsProvider(context)) ||
(Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION.equals(intent.getAction()) &&
isRelevant(context, intent)))
{
Log.w(TAG, "Relevant!");
int subscriptionId = intent.getExtras().getInt("subscription", -1);
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MmsReceiveJob(context, intent.getByteArrayExtra("data"), subscriptionId));
abortBroadcast();
}
}
}
@@ -1,67 +0,0 @@
package org.thoughtcrime.securesms.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.SmsMessage;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.SmsSentJob;
public class SmsDeliveryListener extends BroadcastReceiver {
private static final String TAG = SmsDeliveryListener.class.getSimpleName();
public static final String SENT_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SENT_SMS_ACTION";
public static final String DELIVERED_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DELIVERED_SMS_ACTION";
@Override
public void onReceive(Context context, Intent intent) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
long messageId = intent.getLongExtra("message_id", -1);
switch (intent.getAction()) {
case SENT_SMS_ACTION:
int result = getResultCode();
jobManager.add(new SmsSentJob(context, messageId, SENT_SMS_ACTION, result));
break;
case DELIVERED_SMS_ACTION:
byte[] pdu = intent.getByteArrayExtra("pdu");
if (pdu == null) {
Log.w(TAG, "No PDU in delivery receipt!");
break;
}
SmsMessage message = SmsMessage.createFromPdu(pdu);
if (message == null) {
Log.w(TAG, "Delivery receipt failed to parse!");
break;
}
int status = message.getStatus();
Log.w(TAG, "Original status: " + status);
// Note: https://developer.android.com/reference/android/telephony/SmsMessage.html#getStatus()
// " CDMA: For not interfering with status codes from GSM, the value is shifted to the bits 31-16"
// Note: https://stackoverflow.com/a/33240109
if ("3gpp2".equals(intent.getStringExtra("format"))) {
Log.w(TAG, "Correcting for CDMA delivery receipt...");
if (status >> 24 <= 0) status = SmsDatabase.Status.STATUS_COMPLETE;
else if (status >> 24 == 2) status = SmsDatabase.Status.STATUS_PENDING;
else if (status >> 24 == 3) status = SmsDatabase.Status.STATUS_FAILED;
}
jobManager.add(new SmsSentJob(context, messageId, DELIVERED_SMS_ACTION, status));
break;
default:
Log.w(TAG, "Unknown action: " + intent.getAction());
}
}
}
@@ -17,7 +17,6 @@
package org.thoughtcrime.securesms.sms;
import android.content.Context;
import android.database.Cursor;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
@@ -34,11 +33,9 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
@@ -209,13 +206,13 @@ public class MessageSender {
}
private static void sendSms(Context context, Recipient recipient, long messageId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new SmsSendJob(context, messageId, recipient.getName()));
// JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
// jobManager.add(new SmsSendJob(context, messageId, recipient.getName()));
}
private static void sendMms(Context context, long messageId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new MmsSendJob(context, messageId));
// JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
// jobManager.add(new MmsSendJob(context, messageId));
}
private static boolean isPushTextSend(Context context, Recipient recipient, boolean keyExchange) {
@@ -1,11 +0,0 @@
package org.thoughtcrime.securesms.util;
public class MmsCharacterCalculator extends CharacterCalculator {
private static final int MAX_SIZE = 5000;
@Override
public CharacterState calculateCharacters(String messageBody) {
return new CharacterState(1, MAX_SIZE - messageBody.length(), MAX_SIZE);
}
}
@@ -1,56 +0,0 @@
/**
* 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.util;
import android.telephony.SmsMessage;
import android.util.Log;
public class SmsCharacterCalculator extends CharacterCalculator {
private static final String TAG = SmsCharacterCalculator.class.getSimpleName();
@Override
public CharacterState calculateCharacters(String messageBody) {
int[] length;
int messagesSpent;
int charactersSpent;
int charactersRemaining;
try {
length = SmsMessage.calculateLength(messageBody, false);
messagesSpent = length[0];
charactersSpent = length[1];
charactersRemaining = length[2];
} catch (NullPointerException e) {
Log.w(TAG, e);
messagesSpent = 1;
charactersSpent = messageBody.length();
charactersRemaining = 1000;
}
int maxMessageSize;
if (messagesSpent > 0) {
maxMessageSize = (charactersSpent + charactersRemaining) / messagesSpent;
} else {
maxMessageSize = (charactersSpent + charactersRemaining);
}
return new CharacterState(messagesSpent, charactersRemaining, maxMessageSize);
}
}
@@ -50,7 +50,6 @@ import com.google.i18n.phonenumbers.Phonenumber;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.ByteArrayOutputStream;
@@ -374,11 +373,6 @@ public class Util {
return 90 - age;
}
@TargetApi(VERSION_CODES.LOLLIPOP)
public static boolean isMmsCapable(Context context) {
return (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) || OutgoingLegacyMmsConnection.isConnectionPossible(context);
}
public static boolean isMainThread() {
return Looper.myLooper() == Looper.getMainLooper();
}
@@ -1,16 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.test.InstrumentationTestCase;
public class TextSecureTestCase extends InstrumentationTestCase {
@Override
public void setUp() {
System.setProperty("dexmaker.dexcache", getInstrumentation().getTargetContext().getCacheDir().getPath());
}
protected Context getContext() {
return getInstrumentation().getContext();
}
}
@@ -1,78 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyFloat;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest({ Log.class, Handler.class, Looper.class, TextUtils.class, PreferenceManager.class })
public abstract class BaseUnitTest {
protected Context context = mock(Context.class);
protected SharedPreferences sharedPreferences = mock(SharedPreferences.class);
@Before
public void setUp() throws Exception {
mockStatic(Looper.class);
mockStatic(Log.class);
mockStatic(Handler.class);
mockStatic(TextUtils.class);
mockStatic(PreferenceManager.class);
when(PreferenceManager.getDefaultSharedPreferences(any(Context.class))).thenReturn(sharedPreferences);
when(Looper.getMainLooper()).thenReturn(null);
PowerMockito.whenNew(Handler.class).withAnyArguments().thenReturn(null);
Answer<?> logAnswer = new Answer<Void>() {
@Override public Void answer(InvocationOnMock invocation) throws Throwable {
final String tag = (String)invocation.getArguments()[0];
final String msg = (String)invocation.getArguments()[1];
System.out.println(invocation.getMethod().getName().toUpperCase() + "/[" + tag + "] " + msg);
return null;
}
};
PowerMockito.doAnswer(logAnswer).when(Log.class, "d", anyString(), anyString());
PowerMockito.doAnswer(logAnswer).when(Log.class, "i", anyString(), anyString());
PowerMockito.doAnswer(logAnswer).when(Log.class, "w", anyString(), anyString());
PowerMockito.doAnswer(logAnswer).when(Log.class, "e", anyString(), anyString());
PowerMockito.doAnswer(logAnswer).when(Log.class, "wtf", anyString(), anyString());
PowerMockito.doAnswer(new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
final String s = (String)invocation.getArguments()[0];
return s == null || s.length() == 0;
}
}).when(TextUtils.class, "isEmpty", anyString());
when(sharedPreferences.getString(anyString(), anyString())).thenReturn("");
when(sharedPreferences.getLong(anyString(), anyLong())).thenReturn(0L);
when(sharedPreferences.getInt(anyString(), anyInt())).thenReturn(0);
when(sharedPreferences.getBoolean(anyString(), anyBoolean())).thenReturn(false);
when(sharedPreferences.getFloat(anyString(), anyFloat())).thenReturn(0f);
when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPreferences);
when(context.getPackageName()).thenReturn("org.thoughtcrime.securesms");
}
}
@@ -1,59 +0,0 @@
package org.thoughtcrime.securesms.database;
import org.junit.Before;
import org.junit.Test;
import static junit.framework.Assert.assertEquals;
public class AddressTest {
@Before
public void setup() {}
@Test
public void testAddressString() throws Exception {
Address.ExternalAddressFormatter formatter = new Address.ExternalAddressFormatter("+14152222222");
assertEquals(formatter.format("bonbon"), "bonbon");
}
@Test
public void testAddressShortCode() throws Exception {
Address.ExternalAddressFormatter formatter = new Address.ExternalAddressFormatter("+14152222222");
assertEquals(formatter.format("40404"), "40404");
}
@Test
public void testEmailAddress() throws Exception {
Address.ExternalAddressFormatter formatter = new Address.ExternalAddressFormatter("+14152222222");
assertEquals(formatter.format("junk@junk.net"), "junk@junk.net");
}
@Test
public void testNumberArbitrary() throws Exception {
Address.ExternalAddressFormatter formatter = new Address.ExternalAddressFormatter("+14152222222");
assertEquals(formatter.format("(415) 111-1122"), "+14151111122");
assertEquals(formatter.format("(415) 111 1123"), "+14151111123");
assertEquals(formatter.format("415-111-1124"), "+14151111124");
assertEquals(formatter.format("415.111.1125"), "+14151111125");
assertEquals(formatter.format("+1 415.111.1126"), "+14151111126");
assertEquals(formatter.format("+1 415 111 1127"), "+14151111127");
assertEquals(formatter.format("+1 (415) 111 1128"), "+14151111128");
formatter = new Address.ExternalAddressFormatter("+442079460010");
assertEquals(formatter.format("(020) 7946 0018"), "+442079460018");
}
@Test
public void testGroup() throws Exception {
Address.ExternalAddressFormatter formatter = new Address.ExternalAddressFormatter("+14152222222");
assertEquals(formatter.format("__textsecure_group__!foobar"), "__textsecure_group__!foobar");
}
@Test
public void testLostLocalNumber() throws Exception {
Address.ExternalAddressFormatter formatter = new Address.ExternalAddressFormatter("US", true);
assertEquals(formatter.format("(415) 111-1122"), "+14151111122");
}
}
@@ -1,78 +0,0 @@
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.View;
import android.view.ViewGroup;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class CursorRecyclerViewAdapterTest {
private CursorRecyclerViewAdapter adapter;
private Context context;
private Cursor cursor;
@Before
public void setUp() {
context = mock(Context.class);
cursor = mock(Cursor.class);
when(cursor.getCount()).thenReturn(100);
when(cursor.moveToPosition(anyInt())).thenReturn(true);
adapter = new CursorRecyclerViewAdapter(context, cursor) {
@Override
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
return null;
}
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
}
};
}
@Test
public void testSanityCount() throws Exception {
assertEquals(adapter.getItemCount(), 100);
}
@Test
public void testHeaderCount() throws Exception {
adapter.setHeaderView(new View(context));
assertEquals(adapter.getItemCount(), 101);
assertEquals(adapter.getItemViewType(0), CursorRecyclerViewAdapter.HEADER_TYPE);
assertNotEquals(adapter.getItemViewType(1), CursorRecyclerViewAdapter.HEADER_TYPE);
assertNotEquals(adapter.getItemViewType(100), CursorRecyclerViewAdapter.HEADER_TYPE);
}
@Test
public void testFooterCount() throws Exception {
adapter.setFooterView(new View(context));
assertEquals(adapter.getItemCount(), 101);
assertEquals(adapter.getItemViewType(100), CursorRecyclerViewAdapter.FOOTER_TYPE);
assertNotEquals(adapter.getItemViewType(0), CursorRecyclerViewAdapter.FOOTER_TYPE);
assertNotEquals(adapter.getItemViewType(99), CursorRecyclerViewAdapter.FOOTER_TYPE);
}
@Test
public void testHeaderFooterCount() throws Exception {
adapter.setHeaderView(new View(context));
adapter.setFooterView(new View(context));
assertEquals(adapter.getItemCount(), 102);
assertEquals(adapter.getItemViewType(101), CursorRecyclerViewAdapter.FOOTER_TYPE);
assertEquals(adapter.getItemViewType(0), CursorRecyclerViewAdapter.HEADER_TYPE);
assertNotEquals(adapter.getItemViewType(1), CursorRecyclerViewAdapter.HEADER_TYPE);
assertNotEquals(adapter.getItemViewType(100), CursorRecyclerViewAdapter.FOOTER_TYPE);
}
}
@@ -1,58 +0,0 @@
package org.thoughtcrime.securesms.service;
import junit.framework.AssertionFailedError;
import org.junit.Before;
import org.junit.Test;
import org.thoughtcrime.securesms.BaseUnitTest;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.contains;
import static org.mockito.Mockito.when;
public class SmsListenerTest extends BaseUnitTest {
private static Map<String, String> CHALLENGES = new HashMap<String,String>() {{
put("Your TextSecure verification code: 337-337", "337337");
put("XXX\nYour TextSecure verification code: 1337-1337", "13371337");
put("Your TextSecure verification code: 337-1337", "3371337");
put("Your TextSecure verification code: 1337-337", "1337337");
put("Your TextSecure verification code: 1337-1337", "13371337");
put("XXXYour TextSecure verification code: 1337-1337", "13371337");
put("Your TextSecure verification code: 1337-1337XXX", "13371337");
put("Your TextSecure verification code 1337-1337", "13371337");
put("Your Signal verification code: 337-337", "337337");
put("XXX\nYour Signal verification code: 1337-1337", "13371337");
put("Your Signal verification code: 337-1337", "3371337");
put("Your Signal verification code: 1337-337", "1337337");
put("Your Signal verification code: 1337-1337", "13371337");
put("XXXYour Signal verification code: 1337-1337", "13371337");
put("Your Signal verification code: 1337-1337XXX", "13371337");
put("Your Signal verification code 1337-1337", "13371337");
}};
private SmsListener listener;
@Before
@Override
public void setUp() throws Exception {
super.setUp();
listener = new SmsListener();
when(sharedPreferences.getBoolean(contains("pref_verifying"), anyBoolean())).thenReturn(true);
}
@Test
public void testChallenges() throws Exception {
for (Entry<String,String> challenge : CHALLENGES.entrySet()) {
if (!listener.isChallenge(context, challenge.getKey())) {
throw new AssertionFailedError("SmsListener didn't recognize body as a challenge.");
}
assertEquals(listener.parseChallenge(challenge.getKey()), challenge.getValue());
}
}
}
@@ -1,56 +0,0 @@
package org.thoughtcrime.securesms.util;
import org.junit.Before;
import org.junit.Test;
import static junit.framework.Assert.assertEquals;
public class DelimiterUtilTest {
@Before
public void setup() {}
@Test
public void testEscape() {
assertEquals(DelimiterUtil.escape("MTV Music", ' '), "MTV\\ Music");
assertEquals(DelimiterUtil.escape("MTV Music", ' '), "MTV\\ \\ Music");
assertEquals(DelimiterUtil.escape("MTV,Music", ','), "MTV\\,Music");
assertEquals(DelimiterUtil.escape("MTV,,Music", ','), "MTV\\,\\,Music");
assertEquals(DelimiterUtil.escape("MTV Music", '+'), "MTV Music");
}
@Test
public void testSplit() {
String[] parts = DelimiterUtil.split("MTV\\ Music", ' ');
assertEquals(parts.length, 1);
assertEquals(parts[0], "MTV\\ Music");
parts = DelimiterUtil.split("MTV Music", ' ');
assertEquals(parts.length, 2);
assertEquals(parts[0], "MTV");
assertEquals(parts[1], "Music");
}
@Test
public void testEscapeSplit() {
String input = "MTV Music";
String intermediate = DelimiterUtil.escape(input, ' ');
String[] parts = DelimiterUtil.split(intermediate, ' ');
assertEquals(parts.length, 1);
assertEquals(parts[0], "MTV\\ Music");
assertEquals(DelimiterUtil.unescape(parts[0], ' '), "MTV Music");
input = "MTV\\ Music";
intermediate = DelimiterUtil.escape(input, ' ');
parts = DelimiterUtil.split(intermediate, ' ');
assertEquals(parts.length, 1);
assertEquals(parts[0], "MTV\\\\ Music");
assertEquals(DelimiterUtil.unescape(parts[0], ' '), "MTV\\ Music");
}
}
@@ -1,86 +0,0 @@
package org.thoughtcrime.securesms.util;
import org.junit.Test;
import org.thoughtcrime.securesms.BaseUnitTest;
import java.util.LinkedList;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class ListPartitionTest extends BaseUnitTest {
@Test public void testPartitionEven() {
List<Integer> list = new LinkedList<>();
for (int i=0;i<100;i++) {
list.add(i);
}
List<List<Integer>> partitions = Util.partition(list, 10);
assertEquals(partitions.size(), 10);
int counter = 0;
for (int i=0;i<partitions.size();i++) {
List<Integer> partition = partitions.get(i);
assertEquals(partition.size(), 10);
for (int j=0;j<partition.size();j++) {
assertEquals((int)partition.get(j), counter++);
}
}
}
@Test public void testPartitionOdd() {
List<Integer> list = new LinkedList<>();
for (int i=0;i<100;i++) {
list.add(i);
}
list.add(100);
List<List<Integer>> partitions = Util.partition(list, 10);
assertEquals(partitions.size(), 11);
int counter = 0;
for (int i=0;i<partitions.size()-1;i++) {
List<Integer> partition = partitions.get(i);
assertEquals(partition.size(), 10);
for (int j=0;j<partition.size();j++) {
assertEquals((int)partition.get(j), counter++);
}
}
assertEquals(partitions.get(10).size(), 1);
assertEquals((int)partitions.get(10).get(0), 100);
}
@Test public void testPathological() {
List<Integer> list = new LinkedList<>();
for (int i=0;i<100;i++) {
list.add(i);
}
List<List<Integer>> partitions = Util.partition(list, 1);
assertEquals(partitions.size(), 100);
int counter = 0;
for (int i=0;i<partitions.size();i++) {
List<Integer> partition = partitions.get(i);
assertEquals(partition.size(), 1);
for (int j=0;j<partition.size();j++) {
assertEquals((int)partition.get(j), counter++);
}
}
}
}
@@ -1,67 +0,0 @@
package org.thoughtcrime.securesms.util;
import junit.framework.AssertionFailedError;
import org.junit.Test;
import org.thoughtcrime.securesms.BaseUnitTest;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import static org.assertj.core.api.Assertions.assertThat;
public class PhoneNumberFormatterTest extends BaseUnitTest {
private static final String LOCAL_NUMBER_US = "+15555555555";
private static final String NUMBER_CH = "+41446681800";
private static final String NUMBER_UK = "+442079460018";
private static final String NUMBER_DE = "+4930123456";
private static final String NUMBER_MOBILE_DE = "+49171123456";
private static final String COUNTRY_CODE_CH = "41";
private static final String COUNTRY_CODE_UK = "44";
private static final String COUNTRY_CODE_DE = "49";
@Test
public void testFormatNumber() throws Exception, InvalidNumberException {
assertThat(PhoneNumberFormatter.formatNumber("(555) 555-5555", LOCAL_NUMBER_US)).isEqualTo(LOCAL_NUMBER_US);
assertThat(PhoneNumberFormatter.formatNumber("555-5555", LOCAL_NUMBER_US)).isEqualTo(LOCAL_NUMBER_US);
assertThat(PhoneNumberFormatter.formatNumber("(123) 555-5555", LOCAL_NUMBER_US)).isNotEqualTo(LOCAL_NUMBER_US);
}
@Test
public void testFormatNumberEmail() throws Exception {
try {
PhoneNumberFormatter.formatNumber("person@domain.com", LOCAL_NUMBER_US);
throw new AssertionFailedError("should have thrown on email");
} catch (InvalidNumberException ine) {
// success
}
}
@Test
public void testFormatNumberE164() throws Exception, InvalidNumberException {
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "(020) 7946 0018")).isEqualTo(NUMBER_UK);
// assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "044 20 7946 0018")).isEqualTo(NUMBER_UK);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_UK, "+442079460018")).isEqualTo(NUMBER_UK);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_CH, "+41 44 668 18 00")).isEqualTo(NUMBER_CH);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_CH, "+41 (044) 6681800")).isEqualTo(NUMBER_CH);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049 030 123456")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049 (0)30123456")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049((0)30)123456")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "+49 (0) 30 1 2 3 45 6 ")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "030 123456")).isEqualTo(NUMBER_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0171123456")).isEqualTo(NUMBER_MOBILE_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0171/123456")).isEqualTo(NUMBER_MOBILE_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "+490171/123456")).isEqualTo(NUMBER_MOBILE_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "00490171/123456")).isEqualTo(NUMBER_MOBILE_DE);
assertThat(PhoneNumberFormatter.formatE164(COUNTRY_CODE_DE, "0049171/123456")).isEqualTo(NUMBER_MOBILE_DE);
}
@Test
public void testFormatRemoteNumberE164() throws Exception, InvalidNumberException {
assertThat(PhoneNumberFormatter.formatNumber("+4402079460018", LOCAL_NUMBER_US)).isEqualTo(NUMBER_UK);
}
}
@@ -1,105 +0,0 @@
/*
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.util;
import junit.framework.AssertionFailedError;
import org.junit.Test;
import org.thoughtcrime.securesms.BaseUnitTest;
import java.net.URISyntaxException;
import static org.junit.Assert.assertTrue;
public class Rfc5724UriTest extends BaseUnitTest {
@Test public void testInvalidPath() throws Exception {
final String[] invalidSchemaUris = {
"",
":",
"sms:",
":sms",
"sms:?goto=fail",
"sms:?goto=fail&fail=goto"
};
for (String uri : invalidSchemaUris) {
try {
new Rfc5724Uri(uri);
throw new AssertionFailedError("URISyntaxException should be thrown");
} catch (URISyntaxException e) {
// success
}
}
}
@Test public void testGetSchema() throws Exception {
final String[][] uriTestPairs = {
{"sms:+15555555555", "sms"},
{"sMs:+15555555555", "sMs"},
{"smsto:+15555555555?", "smsto"},
{"mms:+15555555555?a=b", "mms"},
{"mmsto:+15555555555?a=b&c=d", "mmsto"}
};
for (String[] uriTestPair : uriTestPairs) {
final Rfc5724Uri testUri = new Rfc5724Uri(uriTestPair[0]);
assertTrue(testUri.getSchema().equals(uriTestPair[1]));
}
}
@Test public void testGetPath() throws Exception {
final String[][] uriTestPairs = {
{"sms:+15555555555", "+15555555555"},
{"sms:%2B555555555", "%2B555555555"},
{"smsto:+15555555555?", "+15555555555"},
{"mms:+15555555555?a=b", "+15555555555"},
{"mmsto:+15555555555?a=b&c=d", "+15555555555"},
{"sms:+15555555555,+14444444444", "+15555555555,+14444444444"},
{"sms:+15555555555,+14444444444?", "+15555555555,+14444444444"},
{"sms:+15555555555,+14444444444?a=b", "+15555555555,+14444444444"},
{"sms:+15555555555,+14444444444?a=b&c=d", "+15555555555,+14444444444"}
};
for (String[] uriTestPair : uriTestPairs) {
final Rfc5724Uri testUri = new Rfc5724Uri(uriTestPair[0]);
assertTrue(testUri.getPath().equals(uriTestPair[1]));
}
}
@Test public void testGetQueryParams() throws Exception {
final String[][] uriTestPairs = {
{"sms:+15555555555", "a", null},
{"mms:+15555555555?b=", "a", null},
{"mmsto:+15555555555?a=", "a", ""},
{"sms:+15555555555?a=b", "a", "b"},
{"sms:+15555555555?a=b&c=d", "a", "b"},
{"sms:+15555555555?a=b&c=d", "b", null},
{"sms:+15555555555?a=b&c=d", "c", "d"},
{"sms:+15555555555?a=b&c=d", "d", null}
};
for (String[] uriTestPair : uriTestPairs) {
final Rfc5724Uri testUri = new Rfc5724Uri(uriTestPair[0]);
final String paramResult = testUri.getQueryParams().get(uriTestPair[1]);
if (paramResult == null) assertTrue(uriTestPair[2] == null);
else assertTrue(paramResult.equals(uriTestPair[2]));
}
}
}
@@ -1,41 +0,0 @@
package org.thoughtcrime.securesms.util;
import org.junit.Test;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
public class ShortCodeUtilTest {
@Test
public void testShortCodes() throws Exception {
assertTrue(ShortCodeUtil.isShortCode("+14152222222", "40404"));
assertTrue(ShortCodeUtil.isShortCode("+14152222222", "431"));
assertFalse(ShortCodeUtil.isShortCode("+14152222222", "4157778888"));
assertFalse(ShortCodeUtil.isShortCode("+14152222222", "+14157778888"));
assertFalse(ShortCodeUtil.isShortCode("+14152222222", "415-777-8888"));
assertFalse(ShortCodeUtil.isShortCode("+14152222222", "(415) 777-8888"));
assertFalse(ShortCodeUtil.isShortCode("+14152222222", "8882222"));
assertFalse(ShortCodeUtil.isShortCode("+14152222222", "888-2222"));
assertTrue(ShortCodeUtil.isShortCode("+491723742522", "670"));
assertTrue(ShortCodeUtil.isShortCode("+491723742522", "115"));
assertFalse(ShortCodeUtil.isShortCode("+491723742522", "089-12345678"));
assertFalse(ShortCodeUtil.isShortCode("+491723742522", "089/12345678"));
assertFalse(ShortCodeUtil.isShortCode("+491723742522", "12345678"));
assertTrue(ShortCodeUtil.isShortCode("+298123456", "4040"));
assertTrue(ShortCodeUtil.isShortCode("+298123456", "6701"));
assertTrue(ShortCodeUtil.isShortCode("+298123456", "433"));
assertFalse(ShortCodeUtil.isShortCode("+298123456", "123456"));
assertTrue(ShortCodeUtil.isShortCode("+61414915066", "19808948"));
assertFalse(ShortCodeUtil.isShortCode("+61414915066", "119808948"));
assertTrue(ShortCodeUtil.isShortCode("+79166503388", "8080"));
assertTrue(ShortCodeUtil.isShortCode("+79166503388", "6701"));
assertTrue(ShortCodeUtil.isShortCode("+79166503388", "431"));
assertFalse(ShortCodeUtil.isShortCode("+79166503388", "111-22-33"));
}
}