377 lines
16 KiB
Java
Raw Normal View History

/**
* Copyright (C) 2013 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.contacts;
import android.accounts.Account;
import android.content.ContentProviderOperation;
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;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts;
2015-07-14 14:31:03 -07:00
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
2014-11-12 11:15:05 -08:00
import android.text.TextUtils;
2015-07-14 14:31:03 -07:00
import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
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
*
* @author Jake McGinty
*/
public class ContactsDatabase {
2015-07-14 14:31:03 -07:00
private static final String TAG = ContactsDatabase.class.getSimpleName();
2015-07-14 14:31:03 -07:00
public static final String ID_COLUMN = "_id";
public static final String NAME_COLUMN = "name";
public static final String NUMBER_COLUMN = "number";
public static final String NUMBER_TYPE_COLUMN = "number_type";
public static final String LABEL_COLUMN = "label";
public static final String CONTACT_TYPE_COLUMN = "contact_type";
public static final int NORMAL_TYPE = 0;
public static final int PUSH_TYPE = 1;
public static final int NEW_TYPE = 2;
2015-07-14 14:31:03 -07:00
private final Context context;
public ContactsDatabase(Context context) {
this.context = context;
}
public synchronized void setRegisteredUsers(Account account, List<String> e164numbers)
throws RemoteException, OperationApplicationException
{
Map<String, Long> currentContacts = new HashMap<>();
Set<String> registeredNumbers = new HashSet<>(e164numbers);
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;
try {
cursor = context.getContentResolver().query(currentContactsUri, new String[] {BaseColumns._ID, RawContacts.SYNC1}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
currentContacts.put(cursor.getString(1), cursor.getLong(0));
}
} finally {
if (cursor != null)
cursor.close();
}
for (String number : e164numbers) {
if (!currentContacts.containsKey(number)) {
Optional<Pair<String, Long>> systemContactInfo = getSystemContactInfo(number);
if (systemContactInfo.isPresent()) {
addTextSecureRawContact(operations, account, systemContactInfo.get().first, systemContactInfo.get().second);
}
}
}
for (Map.Entry<String, Long> currentContactEntry : currentContacts.entrySet()) {
if (!registeredNumbers.contains(currentContactEntry.getKey())) {
removeTextSecureRawContact(operations, account, currentContactEntry.getValue());
}
}
if (!operations.isEmpty()) {
context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
}
}
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)
2015-07-14 14:31:03 -07:00
.withValue(ContactsContract.Data.SYNC2, "__TS")
.build());
operations.add(ContentProviderOperation.newInsert(dataUri)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
.withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact")
.withValue(ContactsContract.Data.DATA1, e164number)
.withValue(ContactsContract.Data.DATA2, context.getString(R.string.app_name))
2015-07-15 13:19:04 -07:00
.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());
}
2015-07-14 14:31:03 -07:00
public @NonNull Cursor querySystemContacts(String filter) {
Uri uri;
2014-11-12 11:15:05 -08:00
if (!TextUtils.isEmpty(filter)) {
2015-07-14 14:31:03 -07:00
uri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(filter));
} else {
2015-07-14 14:31:03 -07:00
uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
}
2015-07-14 14:31:03 -07:00
String[] projection = new String[]{ContactsContract.CommonDataKinds.Phone._ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.Phone.LABEL};
String sort = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " COLLATE NOCASE ASC";
Map<String, String> projectionMap = new HashMap<String, String>() {{
put(ID_COLUMN, ContactsContract.CommonDataKinds.Phone._ID);
put(NAME_COLUMN, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
put(NUMBER_COLUMN, ContactsContract.CommonDataKinds.Phone.NUMBER);
put(NUMBER_TYPE_COLUMN, ContactsContract.CommonDataKinds.Phone.TYPE);
put(LABEL_COLUMN, ContactsContract.CommonDataKinds.Phone.LABEL);
}};
Cursor cursor = context.getContentResolver().query(uri, projection,
ContactsContract.Data.SYNC2 + " IS NULL OR " +
ContactsContract.Data.SYNC2 + " != ?",
new String[] {"__TS"},
sort);
return new ProjectionMappingCursor(cursor, projectionMap,
new Pair<String, Object>(CONTACT_TYPE_COLUMN, NORMAL_TYPE));
}
2015-07-14 14:31:03 -07:00
public @NonNull Cursor queryTextSecureContacts(String filter) {
String[] projection = new String[] {ContactsContract.Data._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Data.DATA1};
2015-07-14 14:31:03 -07:00
String sort = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE NOCASE ASC";
2015-07-14 14:31:03 -07:00
Map<String, String> projectionMap = new HashMap<String, String>(){{
put(ID_COLUMN, ContactsContract.Data._ID);
put(NAME_COLUMN, ContactsContract.Contacts.DISPLAY_NAME);
put(NUMBER_COLUMN, ContactsContract.Data.DATA1);
}};
2015-07-14 14:31:03 -07:00
Cursor cursor;
2015-07-14 14:31:03 -07:00
if (TextUtils.isEmpty(filter)) {
cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.MIMETYPE + " = ?",
new String[] {"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact"},
sort);
} else {
cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
new String[] {"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact",
"%" + filter + "%"},
sort);
}
2015-07-14 14:31:03 -07:00
return new ProjectionMappingCursor(cursor, projectionMap,
new Pair<String, Object>(LABEL_COLUMN, "TextSecure"),
new Pair<String, Object>(NUMBER_TYPE_COLUMN, 0),
new Pair<String, Object>(CONTACT_TYPE_COLUMN, PUSH_TYPE));
2015-07-14 14:31:03 -07:00
}
2015-07-14 14:31:03 -07:00
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});
2015-07-14 14:31:03 -07:00
return newNumberCursor;
}
private Optional<Pair<String, Long>> getSystemContactInfo(String e164number) {
Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(e164number));
String[] projection = {ContactsContract.PhoneLookup.NUMBER,
ContactsContract.PhoneLookup._ID};
Cursor numberCursor = null;
Cursor idCursor = null;
try {
numberCursor = context.getContentResolver().query(uri, projection, null, null, null);
if (numberCursor != null && numberCursor.moveToNext()) {
idCursor = context.getContentResolver().query(RawContacts.CONTENT_URI,
new String[] {RawContacts._ID},
RawContacts.CONTACT_ID + " = ? ",
new String[] {String.valueOf(numberCursor.getLong(1))},
null);
if (idCursor != null && idCursor.moveToNext()) {
return Optional.of(new Pair<>(numberCursor.getString(0), idCursor.getLong(0)));
}
}
} finally {
if (numberCursor != null) numberCursor.close();
if (idCursor != null) idCursor.close();
}
return Optional.absent();
}
2015-07-14 14:31:03 -07:00
private static class ProjectionMappingCursor extends CursorWrapper {
2015-07-14 14:31:03 -07:00
private final Map<String, String> projectionMap;
private final Pair<String, Object>[] extras;
2015-07-14 14:31:03 -07:00
@SafeVarargs
public ProjectionMappingCursor(Cursor cursor,
Map<String, String> projectionMap,
Pair<String, Object>... extras)
{
super(cursor);
2015-07-14 14:31:03 -07:00
this.projectionMap = projectionMap;
this.extras = extras;
}
@Override
public int getColumnCount() {
2015-07-14 14:31:03 -07:00
return super.getColumnCount() + extras.length;
}
@Override
public int getColumnIndex(String columnName) {
2015-07-14 14:31:03 -07:00
for (int i=0;i<extras.length;i++) {
if (extras[i].first.equals(columnName)) {
return super.getColumnCount() + i;
}
}
return super.getColumnIndex(projectionMap.get(columnName));
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
2015-07-14 14:31:03 -07:00
int index = getColumnIndex(columnName);
if (index == -1) throw new IllegalArgumentException("Bad column name!");
else return index;
}
@Override
public String getColumnName(int columnIndex) {
2015-07-14 14:31:03 -07:00
int baseColumnCount = super.getColumnCount();
if (columnIndex >= baseColumnCount) {
int offset = columnIndex - baseColumnCount;
return extras[offset].first;
}
return getReverseProjection(super.getColumnName(columnIndex));
}
@Override
public String[] getColumnNames() {
2015-07-14 14:31:03 -07:00
String[] names = super.getColumnNames();
String[] allNames = new String[names.length + extras.length];
for (int i=0;i<names.length;i++) {
allNames[i] = getReverseProjection(names[i]);
}
for (int i=0;i<extras.length;i++) {
allNames[names.length + i] = extras[i].first;
}
return allNames;
}
@Override
public int getInt(int columnIndex) {
2015-07-14 14:31:03 -07:00
if (columnIndex >= super.getColumnCount()) {
int offset = columnIndex - super.getColumnCount();
return (Integer)extras[offset].second;
}
return super.getInt(columnIndex);
}
@Override
public String getString(int columnIndex) {
if (columnIndex >= super.getColumnCount()) {
int offset = columnIndex - super.getColumnCount();
return (String)extras[offset].second;
}
return super.getString(columnIndex);
}
private @Nullable String getReverseProjection(String columnName) {
for (Map.Entry<String, String> entry : projectionMap.entrySet()) {
if (entry.getValue().equals(columnName)) {
return entry.getKey();
}
}
return null;
}
}
}