|
|
|
@ -18,33 +18,26 @@ package org.thoughtcrime.securesms.contacts;
|
|
|
|
|
|
|
|
|
|
import android.accounts.Account;
|
|
|
|
|
import android.content.ContentProviderOperation;
|
|
|
|
|
import android.content.ContentValues;
|
|
|
|
|
import android.content.Context;
|
|
|
|
|
import android.content.OperationApplicationException;
|
|
|
|
|
import android.database.Cursor;
|
|
|
|
|
import android.database.CursorWrapper;
|
|
|
|
|
import android.database.MatrixCursor;
|
|
|
|
|
import android.database.MergeCursor;
|
|
|
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
|
|
|
import android.database.sqlite.SQLiteOpenHelper;
|
|
|
|
|
import android.net.Uri;
|
|
|
|
|
import android.os.RemoteException;
|
|
|
|
|
import android.provider.BaseColumns;
|
|
|
|
|
import android.provider.ContactsContract;
|
|
|
|
|
import android.provider.ContactsContract.RawContacts;
|
|
|
|
|
import android.support.annotation.NonNull;
|
|
|
|
|
import android.support.annotation.Nullable;
|
|
|
|
|
import android.text.TextUtils;
|
|
|
|
|
import android.util.Log;
|
|
|
|
|
import android.util.Pair;
|
|
|
|
|
|
|
|
|
|
import org.thoughtcrime.securesms.R;
|
|
|
|
|
import org.thoughtcrime.securesms.util.NumberUtil;
|
|
|
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.Collection;
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.HashSet;
|
|
|
|
|
import java.util.LinkedList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Set;
|
|
|
|
@ -55,46 +48,25 @@ import java.util.Set;
|
|
|
|
|
* @author Jake McGinty
|
|
|
|
|
*/
|
|
|
|
|
public class ContactsDatabase {
|
|
|
|
|
|
|
|
|
|
private static final String TAG = ContactsDatabase.class.getSimpleName();
|
|
|
|
|
private final DatabaseOpenHelper dbHelper;
|
|
|
|
|
private final Context context;
|
|
|
|
|
|
|
|
|
|
public static final String TABLE_NAME = "CONTACTS";
|
|
|
|
|
public static final String ID_COLUMN = ContactsContract.CommonDataKinds.Phone._ID;
|
|
|
|
|
public static final String NAME_COLUMN = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME;
|
|
|
|
|
public static final String NUMBER_TYPE_COLUMN = ContactsContract.CommonDataKinds.Phone.TYPE;
|
|
|
|
|
public static final String NUMBER_COLUMN = ContactsContract.CommonDataKinds.Phone.NUMBER;
|
|
|
|
|
public static final String LABEL_COLUMN = ContactsContract.CommonDataKinds.Phone.LABEL;
|
|
|
|
|
public static final String TYPE_COLUMN = "type";
|
|
|
|
|
|
|
|
|
|
private static final String FILTER_SELECTION = NAME_COLUMN + " LIKE ? OR " + NUMBER_COLUMN + " LIKE ?";
|
|
|
|
|
private static final String CONTACT_LIST_SORT = NAME_COLUMN + " COLLATE NOCASE ASC";
|
|
|
|
|
private static final String[] ANDROID_PROJECTION = new String[]{ID_COLUMN,
|
|
|
|
|
NAME_COLUMN,
|
|
|
|
|
NUMBER_TYPE_COLUMN,
|
|
|
|
|
LABEL_COLUMN,
|
|
|
|
|
NUMBER_COLUMN};
|
|
|
|
|
|
|
|
|
|
private static final String[] CONTACTS_PROJECTION = new String[]{ID_COLUMN,
|
|
|
|
|
NAME_COLUMN,
|
|
|
|
|
NUMBER_TYPE_COLUMN,
|
|
|
|
|
LABEL_COLUMN,
|
|
|
|
|
NUMBER_COLUMN,
|
|
|
|
|
TYPE_COLUMN};
|
|
|
|
|
|
|
|
|
|
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 GROUP_TYPE = 2;
|
|
|
|
|
|
|
|
|
|
private final Context context;
|
|
|
|
|
|
|
|
|
|
public ContactsDatabase(Context context) {
|
|
|
|
|
this.dbHelper = new DatabaseOpenHelper(context);
|
|
|
|
|
this.context = context;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void close() {
|
|
|
|
|
dbHelper.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public synchronized void setRegisteredUsers(Account account, List<String> e164numbers)
|
|
|
|
|
throws RemoteException, OperationApplicationException
|
|
|
|
|
{
|
|
|
|
@ -153,6 +125,7 @@ public class ContactsDatabase {
|
|
|
|
|
.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.Data.SYNC2, "__TS")
|
|
|
|
|
.build());
|
|
|
|
|
|
|
|
|
|
operations.add(ContentProviderOperation.newInsert(dataUri)
|
|
|
|
@ -176,183 +149,184 @@ public class ContactsDatabase {
|
|
|
|
|
.withSelection(BaseColumns._ID + " = ?", new String[] {String.valueOf(rowId)})
|
|
|
|
|
.build());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public @NonNull Cursor querySystemContacts(String filter) {
|
|
|
|
|
Uri uri;
|
|
|
|
|
|
|
|
|
|
public Cursor query(String filter, boolean pushOnly) {
|
|
|
|
|
// FIXME: This doesn't make sense to me. You pass in pushOnly, but then
|
|
|
|
|
// conditionally check to see whether other contacts should be included
|
|
|
|
|
// in the query method itself? I don't think this method should have any
|
|
|
|
|
// understanding of that stuff.
|
|
|
|
|
final boolean includeAndroidContacts = !pushOnly && TextSecurePreferences.isSmsEnabled(context);
|
|
|
|
|
final Cursor localCursor = queryLocalDb(filter);
|
|
|
|
|
final Cursor androidCursor;
|
|
|
|
|
final MatrixCursor newNumberCursor;
|
|
|
|
|
|
|
|
|
|
if (includeAndroidContacts) {
|
|
|
|
|
androidCursor = queryAndroidDb(filter);
|
|
|
|
|
} else {
|
|
|
|
|
androidCursor = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!TextUtils.isEmpty(filter) && NumberUtil.isValidSmsOrEmail(filter)) {
|
|
|
|
|
newNumberCursor = new MatrixCursor(CONTACTS_PROJECTION, 1);
|
|
|
|
|
newNumberCursor.addRow(new Object[]{-1L, context.getString(R.string.contact_selection_list__unknown_contact),
|
|
|
|
|
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, "\u21e2", filter, NORMAL_TYPE});
|
|
|
|
|
} else {
|
|
|
|
|
newNumberCursor = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<Cursor> cursors = new ArrayList<Cursor>();
|
|
|
|
|
if (localCursor != null) cursors.add(localCursor);
|
|
|
|
|
if (androidCursor != null) cursors.add(androidCursor);
|
|
|
|
|
if (newNumberCursor != null) cursors.add(newNumberCursor);
|
|
|
|
|
|
|
|
|
|
switch (cursors.size()) {
|
|
|
|
|
case 0: return null;
|
|
|
|
|
case 1: return cursors.get(0);
|
|
|
|
|
default: return new MergeCursor(cursors.toArray(new Cursor[]{}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Cursor queryAndroidDb(String filter) {
|
|
|
|
|
final Uri baseUri;
|
|
|
|
|
if (!TextUtils.isEmpty(filter)) {
|
|
|
|
|
baseUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI,
|
|
|
|
|
Uri.encode(filter));
|
|
|
|
|
uri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(filter));
|
|
|
|
|
} else {
|
|
|
|
|
baseUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
|
|
|
|
|
uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
|
|
|
|
|
}
|
|
|
|
|
Cursor cursor = context.getContentResolver().query(baseUri, ANDROID_PROJECTION, null, null, CONTACT_LIST_SORT);
|
|
|
|
|
return cursor == null ? null : new TypedCursorWrapper(cursor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Cursor queryLocalDb(String filter) {
|
|
|
|
|
final String selection;
|
|
|
|
|
final String[] selectionArgs;
|
|
|
|
|
final String fuzzyFilter = "%" + filter + "%";
|
|
|
|
|
if (!TextUtils.isEmpty(filter)) {
|
|
|
|
|
selection = FILTER_SELECTION;
|
|
|
|
|
selectionArgs = new String[]{fuzzyFilter, fuzzyFilter};
|
|
|
|
|
} else {
|
|
|
|
|
selection = null;
|
|
|
|
|
selectionArgs = null;
|
|
|
|
|
}
|
|
|
|
|
return queryLocalDb(selection, selectionArgs, null);
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Cursor queryLocalDb(String selection, String[] selectionArgs, String[] columns) {
|
|
|
|
|
SQLiteDatabase localDb = dbHelper.getReadableDatabase();
|
|
|
|
|
final Cursor localCursor;
|
|
|
|
|
if (localDb != null) localCursor = localDb.query(TABLE_NAME, columns, selection, selectionArgs, null, null, CONTACT_LIST_SORT);
|
|
|
|
|
else localCursor = null;
|
|
|
|
|
if (localCursor != null && !localCursor.moveToFirst()) {
|
|
|
|
|
localCursor.close();
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return localCursor;
|
|
|
|
|
}
|
|
|
|
|
public @NonNull Cursor queryTextSecureContacts(String filter) {
|
|
|
|
|
String[] projection = new String[] {ContactsContract.Data._ID,
|
|
|
|
|
ContactsContract.Contacts.DISPLAY_NAME,
|
|
|
|
|
ContactsContract.Data.DATA1};
|
|
|
|
|
|
|
|
|
|
private static class DatabaseOpenHelper extends SQLiteOpenHelper {
|
|
|
|
|
String sort = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE NOCASE ASC";
|
|
|
|
|
|
|
|
|
|
private final Context context;
|
|
|
|
|
private SQLiteDatabase mDatabase;
|
|
|
|
|
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);
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
private static final String TABLE_CREATE =
|
|
|
|
|
"CREATE TABLE " + TABLE_NAME + " (" +
|
|
|
|
|
ID_COLUMN + " INTEGER PRIMARY KEY, " +
|
|
|
|
|
NAME_COLUMN + " TEXT, " +
|
|
|
|
|
NUMBER_TYPE_COLUMN + " INTEGER, " +
|
|
|
|
|
LABEL_COLUMN + " TEXT, " +
|
|
|
|
|
NUMBER_COLUMN + " TEXT, " +
|
|
|
|
|
TYPE_COLUMN + " INTEGER);";
|
|
|
|
|
Cursor cursor;
|
|
|
|
|
|
|
|
|
|
DatabaseOpenHelper(Context context) {
|
|
|
|
|
super(context, null, null, 1);
|
|
|
|
|
this.context = context;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onCreate(SQLiteDatabase db) {
|
|
|
|
|
Log.d(TAG, "onCreate called for contacts database.");
|
|
|
|
|
mDatabase = db;
|
|
|
|
|
mDatabase.execSQL(TABLE_CREATE);
|
|
|
|
|
if (TextSecurePreferences.isPushRegistered(context)) {
|
|
|
|
|
try {
|
|
|
|
|
loadPushUsers();
|
|
|
|
|
} catch (IOException ioe) {
|
|
|
|
|
Log.e(TAG, "Issue when trying to load push users into memory db.", ioe);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
|
|
|
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
|
|
|
|
|
+ newVersion + ", which will destroy all old data");
|
|
|
|
|
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
|
|
|
|
onCreate(db);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void loadPushUsers() throws IOException {
|
|
|
|
|
Log.d(TAG, "populating push users into virtual db.");
|
|
|
|
|
Collection<ContactAccessor.ContactData> pushUsers = ContactAccessor.getInstance().getContactsWithPush(context);
|
|
|
|
|
for (ContactAccessor.ContactData user : pushUsers) {
|
|
|
|
|
ContentValues values = new ContentValues();
|
|
|
|
|
values.put(ID_COLUMN, user.id);
|
|
|
|
|
values.put(NAME_COLUMN, user.name);
|
|
|
|
|
values.put(NUMBER_TYPE_COLUMN, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM);
|
|
|
|
|
values.put(LABEL_COLUMN, (String)null);
|
|
|
|
|
values.put(NUMBER_COLUMN, user.numbers.get(0).number);
|
|
|
|
|
values.put(TYPE_COLUMN, PUSH_TYPE);
|
|
|
|
|
mDatabase.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
|
|
|
|
}
|
|
|
|
|
Log.d(TAG, "finished populating push users.");
|
|
|
|
|
}
|
|
|
|
|
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", NORMAL_TYPE});
|
|
|
|
|
|
|
|
|
|
return newNumberCursor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static class TypedCursorWrapper extends CursorWrapper {
|
|
|
|
|
private static class ProjectionMappingCursor extends CursorWrapper {
|
|
|
|
|
|
|
|
|
|
private final int pushColumnIndex;
|
|
|
|
|
private final Map<String, String> projectionMap;
|
|
|
|
|
private final Pair<String, Object>[] extras;
|
|
|
|
|
|
|
|
|
|
public TypedCursorWrapper(Cursor cursor) {
|
|
|
|
|
@SafeVarargs
|
|
|
|
|
public ProjectionMappingCursor(Cursor cursor,
|
|
|
|
|
Map<String, String> projectionMap,
|
|
|
|
|
Pair<String, Object>... extras)
|
|
|
|
|
{
|
|
|
|
|
super(cursor);
|
|
|
|
|
pushColumnIndex = cursor.getColumnCount();
|
|
|
|
|
this.projectionMap = projectionMap;
|
|
|
|
|
this.extras = extras;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getColumnCount() {
|
|
|
|
|
return super.getColumnCount() + 1;
|
|
|
|
|
return super.getColumnCount() + extras.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getColumnIndex(String columnName) {
|
|
|
|
|
if (TYPE_COLUMN.equals(columnName)) return super.getColumnCount();
|
|
|
|
|
else return super.getColumnIndex(columnName);
|
|
|
|
|
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 {
|
|
|
|
|
if (TYPE_COLUMN.equals(columnName)) return super.getColumnCount();
|
|
|
|
|
else return super.getColumnIndexOrThrow(columnName);
|
|
|
|
|
int index = getColumnIndex(columnName);
|
|
|
|
|
|
|
|
|
|
if (index == -1) throw new IllegalArgumentException("Bad column name!");
|
|
|
|
|
else return index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String getColumnName(int columnIndex) {
|
|
|
|
|
if (columnIndex == pushColumnIndex) return TYPE_COLUMN;
|
|
|
|
|
else return super.getColumnName(columnIndex);
|
|
|
|
|
int baseColumnCount = super.getColumnCount();
|
|
|
|
|
|
|
|
|
|
if (columnIndex >= baseColumnCount) {
|
|
|
|
|
int offset = columnIndex - baseColumnCount;
|
|
|
|
|
return extras[offset].first;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getReverseProjection(super.getColumnName(columnIndex));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String[] getColumnNames() {
|
|
|
|
|
final String[] columns = new String[super.getColumnCount() + 1];
|
|
|
|
|
System.arraycopy(super.getColumnNames(), 0, columns, 0, super.getColumnCount());
|
|
|
|
|
columns[pushColumnIndex] = TYPE_COLUMN;
|
|
|
|
|
return columns;
|
|
|
|
|
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) {
|
|
|
|
|
if (columnIndex == pushColumnIndex) return NORMAL_TYPE;
|
|
|
|
|
else return super.getInt(columnIndex);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|