diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e08c6cdc67..96b04318ba 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -317,6 +317,19 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9174a2376e..c806d57e61 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -70,6 +70,7 @@
Message %s
+ Signal Call %s
Message size: %d KB
diff --git a/res/xml/contactsformat.xml b/res/xml/contactsformat.xml
index 3857e2a44a..7aaceef0be 100644
--- a/res/xml/contactsformat.xml
+++ b/res/xml/contactsformat.xml
@@ -6,4 +6,9 @@
android:summaryColumn="data2"
android:detailColumn="data3"
android:detailSocialSummary="true"/>
+
diff --git a/src/org/thoughtcrime/redphone/RedPhoneShare.java b/src/org/thoughtcrime/redphone/RedPhoneShare.java
new file mode 100644
index 0000000000..54938775d6
--- /dev/null
+++ b/src/org/thoughtcrime/redphone/RedPhoneShare.java
@@ -0,0 +1,46 @@
+package org.thoughtcrime.redphone;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+
+public class RedPhoneShare extends Activity {
+
+ private static final String TAG = RedPhone.class.getSimpleName();
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ if (getIntent().getData() != null && "content".equals(getIntent().getData().getScheme())) {
+ Cursor cursor = null;
+
+ try {
+ cursor = getContentResolver().query(getIntent().getData(), null, null, null, null);
+
+ if (cursor != null && cursor.moveToNext()) {
+ String destination = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1));
+
+ if (!TextUtils.isEmpty(destination)) {
+ Intent serviceIntent = new Intent(this, RedPhoneService.class);
+ serviceIntent.setAction(RedPhoneService.ACTION_OUTGOING_CALL);
+ serviceIntent.putExtra(RedPhoneService.EXTRA_REMOTE_NUMBER, destination);
+ startService(serviceIntent);
+
+ Intent activityIntent = new Intent(this, RedPhone.class);
+ activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(activityIntent);
+ }
+ }
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+
+ finish();
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java b/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
index 5339e28e6c..391d27fff5 100644
--- a/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
+++ b/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java
@@ -20,11 +20,13 @@ import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
+import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.v4.content.CursorLoader;
import android.text.TextUtils;
import android.util.Log;
+import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
@@ -73,7 +75,18 @@ public class ContactsCursorLoader extends CursorLoader {
}
if (!TextUtils.isEmpty(filter) && NumberUtil.isValidSmsOrEmail(filter)) {
- cursorList.add(contactsDatabase.getNewNumberCursor(filter));
+ MatrixCursor newNumberCursor = new MatrixCursor(new String[] {ContactsDatabase.ID_COLUMN,
+ ContactsDatabase.NAME_COLUMN,
+ ContactsDatabase.NUMBER_COLUMN,
+ ContactsDatabase.NUMBER_TYPE_COLUMN,
+ ContactsDatabase.LABEL_COLUMN,
+ ContactsDatabase.CONTACT_TYPE_COLUMN}, 1);
+
+ newNumberCursor.addRow(new Object[] {-1L, getContext().getString(R.string.contact_selection_list__unknown_contact),
+ filter, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
+ "\u21e2", ContactsDatabase.NEW_TYPE});
+
+ cursorList.add(newNumberCursor);
}
return new MergeCursor(cursorList.toArray(new Cursor[0]));
diff --git a/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java b/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java
index 47d7f490bd..a8f3204d37 100644
--- a/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java
+++ b/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java
@@ -22,7 +22,6 @@ import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.CursorWrapper;
-import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
@@ -37,16 +36,15 @@ import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.whispersystems.libaxolotl.util.guava.Optional;
+import org.whispersystems.textsecure.api.push.ContactTokenDetails;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/**
* Database to supply all types of contacts that TextSecure needs to know about
@@ -55,9 +53,10 @@ import java.util.Set;
*/
public class ContactsDatabase {
- private static final String TAG = ContactsDatabase.class.getSimpleName();
- private static final String MIME = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact";
- private static final String SYNC = "__TS";
+ private static final String TAG = ContactsDatabase.class.getSimpleName();
+ private static final String CONTACT_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact";
+ private static final String CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call";
+ private static final String SYNC = "__TS";
public static final String ID_COLUMN = "_id";
public static final String NAME_COLUMN = "name";
@@ -78,54 +77,44 @@ public class ContactsDatabase {
public synchronized @NonNull List setRegisteredUsers(@NonNull Account account,
@NonNull String localNumber,
- @NonNull List e164numbers)
+ @NonNull List registeredContacts)
throws RemoteException, OperationApplicationException
{
- Map currentContacts = new HashMap<>();
- Set registeredNumbers = new HashSet<>(e164numbers);
- List addedNumbers = new LinkedList<>();
- ArrayList operations = new ArrayList<>();
- Uri currentContactsUri = RawContacts.CONTENT_URI.buildUpon()
- .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type).build();
- Cursor cursor = null;
+ Map registeredNumbers = new HashMap<>();
+ List addedNumbers = new LinkedList<>();
+ ArrayList operations = new ArrayList<>();
+ Map currentContacts = getSignalRawContacts(account, localNumber);
- try {
- cursor = context.getContentResolver().query(currentContactsUri, new String[] {BaseColumns._ID, RawContacts.SYNC1}, null, null, null);
+ for (ContactTokenDetails registeredContact : registeredContacts) {
+ String registeredNumber = registeredContact.getNumber();
- while (cursor != null && cursor.moveToNext()) {
- String currentNumber;
+ registeredNumbers.put(registeredNumber, registeredContact);
- try {
- currentNumber = PhoneNumberFormatter.formatNumber(cursor.getString(1), localNumber);
- } catch (InvalidNumberException e) {
- Log.w(TAG, e);
- currentNumber = cursor.getString(1);
- }
-
- currentContacts.put(currentNumber, cursor.getLong(0));
- }
- } finally {
- if (cursor != null)
- cursor.close();
- }
-
- for (String number : e164numbers) {
- if (!currentContacts.containsKey(number)) {
- Optional systemContactInfo = getSystemContactInfo(number, localNumber);
+ if (!currentContacts.containsKey(registeredNumber)) {
+ Optional systemContactInfo = getSystemContactInfo(registeredNumber, localNumber);
if (systemContactInfo.isPresent()) {
- Log.w(TAG, "Adding number: " + number);
- addedNumbers.add(number);
- addTextSecureRawContact(operations, account, systemContactInfo.get().number, systemContactInfo.get().id);
+ Log.w(TAG, "Adding number: " + registeredNumber);
+ addedNumbers.add(registeredNumber);
+ addTextSecureRawContact(operations, account, systemContactInfo.get().number,
+ systemContactInfo.get().id, registeredContact.isVoice());
}
}
}
- for (Map.Entry currentContactEntry : currentContacts.entrySet()) {
- if (!registeredNumbers.contains(currentContactEntry.getKey())) {
- removeTextSecureRawContact(operations, account, currentContactEntry.getValue());
+ for (Map.Entry currentContactEntry : currentContacts.entrySet()) {
+ ContactTokenDetails tokenDetails = registeredNumbers.get(currentContactEntry.getKey());
+
+ if (tokenDetails == null) {
+ Log.w(TAG, "Removing number: " + currentContactEntry.getKey());
+ removeTextSecureRawContact(operations, account, currentContactEntry.getValue().getId());
+ } else if (tokenDetails.isVoice() && !currentContactEntry.getValue().isVoiceSupported()) {
+ Log.w(TAG, "Adding voice support: " + currentContactEntry.getKey());
+ addContactVoiceSupport(operations, currentContactEntry.getKey(), currentContactEntry.getValue().getId());
+ } else if (!tokenDetails.isVoice() && currentContactEntry.getValue().isVoiceSupported()) {
+ Log.w(TAG, "Removing voice support: " + currentContactEntry.getKey());
+ removeContactVoiceSupport(operations, currentContactEntry.getValue().getId());
}
}
@@ -136,60 +125,6 @@ public class ContactsDatabase {
return addedNumbers;
}
- private void addTextSecureRawContact(List operations,
- Account account,
- String e164number,
- long aggregateId)
- {
- int index = operations.size();
- Uri dataUri = ContactsContract.Data.CONTENT_URI.buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
- .build();
-
- operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
- .withValue(RawContacts.ACCOUNT_NAME, account.name)
- .withValue(RawContacts.ACCOUNT_TYPE, account.type)
- .withValue(RawContacts.SYNC1, e164number)
- .build());
-
- operations.add(ContentProviderOperation.newInsert(dataUri)
- .withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, index)
- .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
- .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, e164number)
- .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_OTHER)
- .withValue(ContactsContract.Data.SYNC2, SYNC)
- .build());
-
- operations.add(ContentProviderOperation.newInsert(dataUri)
- .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
- .withValue(ContactsContract.Data.MIMETYPE, MIME)
- .withValue(ContactsContract.Data.DATA1, e164number)
- .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
- .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_message_s, e164number))
- .withYieldAllowed(true)
- .build());
-
- if (Build.VERSION.SDK_INT >= 11) {
- operations.add(ContentProviderOperation.newUpdate(ContactsContract.AggregationExceptions.CONTENT_URI)
- .withValue(ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, aggregateId)
- .withValueBackReference(ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, index)
- .withValue(ContactsContract.AggregationExceptions.TYPE, ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER)
- .build());
- }
- }
-
- private void removeTextSecureRawContact(List operations,
- Account account, long rowId)
- {
- operations.add(ContentProviderOperation.newDelete(RawContacts.CONTENT_URI.buildUpon()
- .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build())
- .withYieldAllowed(true)
- .withSelection(BaseColumns._ID + " = ?", new String[] {String.valueOf(rowId)})
- .build());
- }
-
public @NonNull Cursor querySystemContacts(String filter) {
Uri uri;
@@ -248,13 +183,13 @@ public class ContactsDatabase {
cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.MIMETYPE + " = ?",
- new String[] {MIME},
+ new String[] {CONTACT_MIMETYPE},
sort);
} else {
cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.MIMETYPE + " = ? AND (" + ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? OR " + ContactsContract.Data.DATA1 + " LIKE ?)",
- new String[] {MIME,
+ new String[] {CONTACT_MIMETYPE,
"%" + filter + "%", "%" + filter + "%"},
sort);
}
@@ -266,13 +201,132 @@ public class ContactsDatabase {
}
- public Cursor getNewNumberCursor(String filter) {
- MatrixCursor newNumberCursor = new MatrixCursor(new String[] {ID_COLUMN, NAME_COLUMN, NUMBER_COLUMN, NUMBER_TYPE_COLUMN, LABEL_COLUMN, CONTACT_TYPE_COLUMN}, 1);
- newNumberCursor.addRow(new Object[]{-1L, context.getString(R.string.contact_selection_list__unknown_contact),
- filter, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
- "\u21e2", NEW_TYPE});
+ private void addContactVoiceSupport(List operations,
+ @NonNull String e164number, long rawContactId)
+ {
+ operations.add(ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+ .withSelection(RawContacts._ID + " = ?", new String[] {String.valueOf(rawContactId)})
+ .withValue(RawContacts.SYNC4, "true")
+ .build());
- return newNumberCursor;
+ operations.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build())
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
+ .withValue(ContactsContract.Data.MIMETYPE, CALL_MIMETYPE)
+ .withValue(ContactsContract.Data.DATA1, e164number)
+ .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_signal_call_s, e164number))
+ .withYieldAllowed(true)
+ .build());
+ }
+
+ private void removeContactVoiceSupport(List operations, long rawContactId) {
+ operations.add(ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+ .withSelection(RawContacts._ID + " = ?", new String[] {String.valueOf(rawContactId)})
+ .withValue(RawContacts.SYNC4, "false")
+ .build());
+
+ operations.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build())
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?",
+ new String[] {String.valueOf(rawContactId), CALL_MIMETYPE})
+ .withYieldAllowed(true)
+ .build());
+ }
+
+ private void addTextSecureRawContact(List operations,
+ Account account, String e164number,
+ long aggregateId, boolean supportsVoice)
+ {
+ int index = operations.size();
+ Uri dataUri = ContactsContract.Data.CONTENT_URI.buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+
+ operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValue(RawContacts.ACCOUNT_NAME, account.name)
+ .withValue(RawContacts.ACCOUNT_TYPE, account.type)
+ .withValue(RawContacts.SYNC1, e164number)
+ .withValue(RawContacts.SYNC4, String.valueOf(supportsVoice))
+ .build());
+
+ operations.add(ContentProviderOperation.newInsert(dataUri)
+ .withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, index)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, e164number)
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_OTHER)
+ .withValue(ContactsContract.Data.SYNC2, SYNC)
+ .build());
+
+ operations.add(ContentProviderOperation.newInsert(dataUri)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(ContactsContract.Data.MIMETYPE, CONTACT_MIMETYPE)
+ .withValue(ContactsContract.Data.DATA1, e164number)
+ .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_message_s, e164number))
+ .withYieldAllowed(true)
+ .build());
+
+ if (supportsVoice) {
+ operations.add(ContentProviderOperation.newInsert(dataUri)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(ContactsContract.Data.MIMETYPE, CALL_MIMETYPE)
+ .withValue(ContactsContract.Data.DATA1, e164number)
+ .withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, context.getString(R.string.ContactsDatabase_signal_call_s, e164number))
+ .withYieldAllowed(true)
+ .build());
+ }
+
+
+ if (Build.VERSION.SDK_INT >= 11) {
+ operations.add(ContentProviderOperation.newUpdate(ContactsContract.AggregationExceptions.CONTENT_URI)
+ .withValue(ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, aggregateId)
+ .withValueBackReference(ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, index)
+ .withValue(ContactsContract.AggregationExceptions.TYPE, ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER)
+ .build());
+ }
+ }
+
+ private void removeTextSecureRawContact(List operations,
+ Account account, long rowId)
+ {
+ operations.add(ContentProviderOperation.newDelete(RawContacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build())
+ .withYieldAllowed(true)
+ .withSelection(BaseColumns._ID + " = ?", new String[] {String.valueOf(rowId)})
+ .build());
+ }
+
+ private @NonNull Map getSignalRawContacts(Account account, String localNumber) {
+ Uri currentContactsUri = RawContacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type).build();
+
+ Map signalContacts = new HashMap<>();
+ Cursor cursor = null;
+
+ try {
+ cursor = context.getContentResolver().query(currentContactsUri, new String[] {BaseColumns._ID, RawContacts.SYNC1, RawContacts.SYNC4}, null, null, null);
+
+ while (cursor != null && cursor.moveToNext()) {
+ String currentNumber;
+
+ try {
+ currentNumber = PhoneNumberFormatter.formatNumber(cursor.getString(1), localNumber);
+ } catch (InvalidNumberException e) {
+ Log.w(TAG, e);
+ currentNumber = cursor.getString(1);
+ }
+
+ signalContacts.put(currentNumber, new SignalContact(cursor.getLong(0), cursor.getString(2)));
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+
+ return signalContacts;
}
private Optional getSystemContactInfo(@NonNull String e164number,
@@ -428,4 +482,22 @@ public class ContactsDatabase {
this.id = id;
}
}
+
+ private static class SignalContact {
+ private final long id;
+ @Nullable private final String supportsVoice;
+
+ public SignalContact(long id, @Nullable String supportsVoice) {
+ this.id = id;
+ this.supportsVoice = supportsVoice;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public boolean isVoiceSupported() {
+ return "true".equals(supportsVoice);
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java
index 3db59d242d..cd484b35cd 100644
--- a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java
+++ b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java
@@ -106,15 +106,9 @@ public class DirectoryHelper {
directory.setNumbers(activeTokens, eligibleContactNumbers);
if (account.isPresent()) {
- List e164numbers = new LinkedList<>();
-
- for (ContactTokenDetails contactTokenDetails : activeTokens) {
- e164numbers.add(contactTokenDetails.getNumber());
- }
-
try {
return DatabaseFactory.getContactsDatabase(context)
- .setRegisteredUsers(account.get(), localNumber, e164numbers);
+ .setRegisteredUsers(account.get(), localNumber, activeTokens);
} catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, e);
}