Add 'Signal Call' option to contact card

Fixes #4392
Closes #4465
// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-11-09 12:30:36 -08:00
parent 5c59c3f423
commit 7bec5efe1a
7 changed files with 256 additions and 112 deletions

View File

@ -317,6 +317,19 @@
</intent-filter>
</activity>
<activity android:name="org.thoughtcrime.redphone.RedPhoneShare"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" />
</intent-filter>
</activity>
<activity android:name=".RecipientPreferenceActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

View File

@ -70,6 +70,7 @@
<!-- ContactsDatabase -->
<string name="ContactsDatabase_message_s">Message %s</string>
<string name="ContactsDatabase_signal_call_s">Signal Call %s</string>
<!-- ConversationItem -->
<string name="ConversationItem_message_size_d_kb">Message size: %d KB</string>

View File

@ -6,4 +6,9 @@
android:summaryColumn="data2"
android:detailColumn="data3"
android:detailSocialSummary="true"/>
<ContactsDataKind
android:icon="@drawable/icon"
android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call"
android:summaryColumn="data2"
android:detailColumn="data3"/>
</ContactsSource>

View File

@ -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();
}
}

View File

@ -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]));

View File

@ -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<String> setRegisteredUsers(@NonNull Account account,
@NonNull String localNumber,
@NonNull List<String> e164numbers)
@NonNull List<ContactTokenDetails> registeredContacts)
throws RemoteException, OperationApplicationException
{
Map<String, Long> currentContacts = new HashMap<>();
Set<String> registeredNumbers = new HashSet<>(e164numbers);
List<String> addedNumbers = new LinkedList<>();
ArrayList<ContentProviderOperation> 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<String, ContactTokenDetails> registeredNumbers = new HashMap<>();
List<String> addedNumbers = new LinkedList<>();
ArrayList<ContentProviderOperation> operations = new ArrayList<>();
Map<String, SignalContact> 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> systemContactInfo = getSystemContactInfo(number, localNumber);
if (!currentContacts.containsKey(registeredNumber)) {
Optional<SystemContactInfo> 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<String, Long> currentContactEntry : currentContacts.entrySet()) {
if (!registeredNumbers.contains(currentContactEntry.getKey())) {
removeTextSecureRawContact(operations, account, currentContactEntry.getValue());
for (Map.Entry<String, SignalContact> 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<ContentProviderOperation> 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<ContentProviderOperation> 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<ContentProviderOperation> 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<ContentProviderOperation> 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<ContentProviderOperation> 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<ContentProviderOperation> 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<String, SignalContact> 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<String, SignalContact> 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<SystemContactInfo> 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);
}
}
}

View File

@ -106,15 +106,9 @@ public class DirectoryHelper {
directory.setNumbers(activeTokens, eligibleContactNumbers);
if (account.isPresent()) {
List<String> 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);
}