/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.dialer.searchfragment.cp2; import android.content.ContentResolver; import android.database.CharArrayBuffer; import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.net.Uri; import android.os.Bundle; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.dialer.searchfragment.common.Projections; import com.android.dialer.searchfragment.common.QueryFilteringUtil; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** * Wrapper for a cursor containing all on device contacts. * *

This cursor removes duplicate phone numbers associated with the same contact and can filter * contacts based on a query by calling {@link #filter(String)}. */ final class ContactFilterCursor implements Cursor { private final Cursor cursor; // List of cursor ids that are valid for displaying after filtering. private final List queryFilteredPositions = new ArrayList<>(); private int currentPosition = 0; @Retention(RetentionPolicy.SOURCE) @IntDef({ Qualification.NUMBERS_ARE_NOT_DUPLICATES, Qualification.NEW_NUMBER_IS_MORE_QUALIFIED, Qualification.CURRENT_MORE_QUALIFIED }) private @interface Qualification { /** Numbers are not duplicates (i.e. neither is more qualified than the other). */ int NUMBERS_ARE_NOT_DUPLICATES = 0; /** Number are duplicates and new number is more qualified than the existing number. */ int NEW_NUMBER_IS_MORE_QUALIFIED = 1; /** Numbers are duplicates but current/existing number is more qualified than new number. */ int CURRENT_MORE_QUALIFIED = 2; } /** * @param cursor with projection {@link Projections#PHONE_PROJECTION}. * @param query to filter cursor results. */ ContactFilterCursor(Cursor cursor, @Nullable String query) { // TODO(calderwoodra) investigate copying this into a MatrixCursor and holding in memory this.cursor = cursor; filter(query); } /** * Filters out contacts that do not match the query. * *

The query can have at least 1 of 3 forms: * *

* *

A contact is considered a match if: * *

*/ public void filter(@Nullable String query) { if (query == null) { query = ""; } queryFilteredPositions.clear(); // On some devices, contacts have multiple rows with identical phone numbers. These numbers are // considered duplicates. Since the order might not be guaranteed, we compare all of the numbers // and hold onto the most qualified one as the one we want to display to the user. // See #getQualification for details on how qualification is determined. int previousMostQualifiedPosition = 0; String previousName = ""; String previousMostQualifiedNumber = ""; query = query.toLowerCase(); cursor.moveToPosition(-1); while (cursor.moveToNext()) { int position = cursor.getPosition(); String currentNumber = cursor.getString(Projections.PHONE_NUMBER); String currentName = cursor.getString(Projections.PHONE_DISPLAY_NAME); if (!previousName.equals(currentName)) { previousName = currentName; previousMostQualifiedNumber = currentNumber; previousMostQualifiedPosition = position; } else { // Since the contact name is the same, check if this number is a duplicate switch (getQualification(currentNumber, previousMostQualifiedNumber)) { case Qualification.CURRENT_MORE_QUALIFIED: // Number is a less qualified duplicate, ignore it. continue; case Qualification.NEW_NUMBER_IS_MORE_QUALIFIED: // If number wasn't filtered out before, remove it and add it's more qualified version. int index = queryFilteredPositions.indexOf(previousMostQualifiedPosition); if (index != -1) { queryFilteredPositions.remove(index); queryFilteredPositions.add(position); } previousMostQualifiedNumber = currentNumber; previousMostQualifiedPosition = position; continue; case Qualification.NUMBERS_ARE_NOT_DUPLICATES: default: previousMostQualifiedNumber = currentNumber; previousMostQualifiedPosition = position; } } if (TextUtils.isEmpty(query) || QueryFilteringUtil.nameMatchesT9Query(query, previousName) || QueryFilteringUtil.numberMatchesNumberQuery(query, previousMostQualifiedNumber) || QueryFilteringUtil.nameContainsQuery(query, previousName)) { queryFilteredPositions.add(previousMostQualifiedPosition); } } currentPosition = 0; cursor.moveToFirst(); } /** * @param number that may or may not be more qualified than the existing most qualified number * @param mostQualifiedNumber currently most qualified number associated with same contact * @return {@link Qualification} where the more qualified number is the number with the most * digits. If the digits are the same, the number with the most formatting is more qualified. */ private @Qualification int getQualification(String number, String mostQualifiedNumber) { // Ignore formatting String numberDigits = QueryFilteringUtil.digitsOnly(number); String qualifiedNumberDigits = QueryFilteringUtil.digitsOnly(mostQualifiedNumber); // If the numbers are identical, return version with more formatting if (qualifiedNumberDigits.equals(numberDigits)) { if (mostQualifiedNumber.length() >= number.length()) { return Qualification.CURRENT_MORE_QUALIFIED; } else { return Qualification.NEW_NUMBER_IS_MORE_QUALIFIED; } } // If one number is a suffix of another, then return the longer one. // If they are equal, then return the current most qualified number. if (qualifiedNumberDigits.endsWith(numberDigits)) { return Qualification.CURRENT_MORE_QUALIFIED; } if (numberDigits.endsWith(qualifiedNumberDigits)) { return Qualification.NEW_NUMBER_IS_MORE_QUALIFIED; } return Qualification.NUMBERS_ARE_NOT_DUPLICATES; } @Override public boolean moveToPosition(int position) { currentPosition = position; return currentPosition < getCount() && cursor.moveToPosition(queryFilteredPositions.get(currentPosition)); } @Override public boolean move(int offset) { currentPosition += offset; return moveToPosition(currentPosition); } @Override public int getCount() { return queryFilteredPositions.size(); } @Override public boolean isFirst() { return currentPosition == 0; } @Override public boolean isLast() { return currentPosition == getCount() - 1; } @Override public int getPosition() { return currentPosition; } @Override public boolean moveToFirst() { return moveToPosition(0); } @Override public boolean moveToLast() { return moveToPosition(getCount() - 1); } @Override public boolean moveToNext() { return moveToPosition(++currentPosition); } @Override public boolean moveToPrevious() { return moveToPosition(--currentPosition); } // Methods below simply call the corresponding method in cursor. @Override public boolean isBeforeFirst() { return cursor.isBeforeFirst(); } @Override public boolean isAfterLast() { return cursor.isAfterLast(); } @Override public int getColumnIndex(String columnName) { return cursor.getColumnIndex(columnName); } @Override public int getColumnIndexOrThrow(String columnName) { return cursor.getColumnIndexOrThrow(columnName); } @Override public String getColumnName(int columnIndex) { return cursor.getColumnName(columnIndex); } @Override public String[] getColumnNames() { return cursor.getColumnNames(); } @Override public int getColumnCount() { return cursor.getColumnCount(); } @Override public byte[] getBlob(int columnIndex) { return cursor.getBlob(columnIndex); } @Override public String getString(int columnIndex) { return cursor.getString(columnIndex); } @Override public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { cursor.copyStringToBuffer(columnIndex, buffer); } @Override public short getShort(int columnIndex) { return cursor.getShort(columnIndex); } @Override public int getInt(int columnIndex) { return cursor.getInt(columnIndex); } @Override public long getLong(int columnIndex) { return cursor.getLong(columnIndex); } @Override public float getFloat(int columnIndex) { return cursor.getFloat(columnIndex); } @Override public double getDouble(int columnIndex) { return cursor.getDouble(columnIndex); } @Override public int getType(int columnIndex) { return cursor.getType(columnIndex); } @Override public boolean isNull(int columnIndex) { return cursor.isNull(columnIndex); } @Override public void deactivate() { cursor.deactivate(); } @Override public boolean requery() { return cursor.requery(); } @Override public void close() { cursor.close(); } @Override public boolean isClosed() { return cursor.isClosed(); } @Override public void registerContentObserver(ContentObserver observer) { cursor.registerContentObserver(observer); } @Override public void unregisterContentObserver(ContentObserver observer) { cursor.unregisterContentObserver(observer); } @Override public void registerDataSetObserver(DataSetObserver observer) { cursor.registerDataSetObserver(observer); } @Override public void unregisterDataSetObserver(DataSetObserver observer) { cursor.unregisterDataSetObserver(observer); } @Override public void setNotificationUri(ContentResolver cr, Uri uri) { cursor.setNotificationUri(cr, uri); } @Override public Uri getNotificationUri() { return cursor.getNotificationUri(); } @Override public boolean getWantsAllOnMoveCalls() { return cursor.getWantsAllOnMoveCalls(); } @Override public void setExtras(Bundle extras) { cursor.setExtras(extras); } @Override public Bundle getExtras() { return cursor.getExtras(); } @Override public Bundle respond(Bundle extras) { return cursor.respond(extras); } }