diff options
Diffstat (limited to 'java')
18 files changed, 736 insertions, 193 deletions
diff --git a/java/com/android/dialer/dialpadview/DialpadAlphabets.java b/java/com/android/dialer/dialpadview/DialpadAlphabets.java deleted file mode 100644 index f02ca4395..000000000 --- a/java/com/android/dialer/dialpadview/DialpadAlphabets.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.dialpadview; - -import android.support.v4.util.SimpleArrayMap; - -/** A class containing key-letter mappings for the dialpad. */ -public class DialpadAlphabets { - - // The default mapping (the Latin alphabet) - private static final String[] def = { - "+" /* 0 */, - "" /* 1 */, - "ABC" /* 2 */, - "DEF" /* 3 */, - "GHI" /* 4 */, - "JKL" /* 5 */, - "MNO" /* 6 */, - "PQRS" /* 7 */, - "TUV" /* 8 */, - "WXYZ" /* 9 */, - "" /* * */, - "" /* # */, - }; - - // Russian - private static final String[] rus = { - "" /* 0 */, - "" /* 1 */, - "АБВГ" /* 2 */, - "ДЕЖЗ" /* 3 */, - "ИЙКЛ" /* 4 */, - "МНОП" /* 5 */, - "РСТУ" /* 6 */, - "ФХЦЧ" /* 7 */, - "ШЩЪЫ" /* 8 */, - "ЬЭЮЯ" /* 9 */, - "" /* * */, - "" /* # */, - }; - - // A map in which each key is an ISO 639-2 language code and the corresponding key is an array - // defining key-letter mappings - private static final SimpleArrayMap<String, String[]> alphabets = new SimpleArrayMap<>(); - - static { - alphabets.put("rus", rus); - } - - /** - * Returns the alphabet (a key-letter mapping) of the given ISO 639-2 language code or null if - * - * <ul> - * <li>no alphabet for the language code is defined, or - * <li>the language code is invalid. - * </ul> - */ - public static String[] getAlphabetForLanguage(String languageCode) { - return alphabets.get(languageCode); - } - - /** Returns the default key-letter mapping (the one that uses the Latin alphabet). */ - public static String[] getDefaultAlphabet() { - return def; - } -} diff --git a/java/com/android/dialer/dialpadview/DialpadCharMappings.java b/java/com/android/dialer/dialpadview/DialpadCharMappings.java new file mode 100644 index 000000000..03bc2e728 --- /dev/null +++ b/java/com/android/dialer/dialpadview/DialpadCharMappings.java @@ -0,0 +1,228 @@ +/* + * 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.dialpadview; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.v4.util.SimpleArrayMap; +import com.android.dialer.common.Assert; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.configprovider.ConfigProviderBindings; + +/** A class containing character mappings for the dialpad. */ +public class DialpadCharMappings { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public static final String FLAG_ENABLE_DUAL_ALPHABETS = "enable_dual_alphabets_on_t9"; + + /** The character mapping for the Latin alphabet (the default mapping) */ + private static class Latin { + private static final String[] KEY_TO_CHARS = { + "+" /* 0 */, + "" /* 1 */, + "ABC" /* 2 */, + "DEF" /* 3 */, + "GHI" /* 4 */, + "JKL" /* 5 */, + "MNO" /* 6 */, + "PQRS" /* 7 */, + "TUV" /* 8 */, + "WXYZ" /* 9 */, + "" /* * */, + "" /* # */, + }; + + private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY = + getCharToKeyMap(KEY_TO_CHARS); + } + + /** The character mapping for the Bulgarian alphabet */ + private static class Bul { + private static final String[] KEY_TO_CHARS = { + "" /* 0 */, + "" /* 1 */, + "АБВГ" /* 2 */, + "ДЕЖЗ" /* 3 */, + "ИЙКЛ" /* 4 */, + "МНО" /* 5 */, + "ПРС" /* 6 */, + "ТУФХ" /* 7 */, + "ЦЧШЩ" /* 8 */, + "ЪЬЮЯ" /* 9 */, + "" /* * */, + "" /* # */, + }; + + private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY = + getCharToKeyMap(KEY_TO_CHARS); + } + + /** The character mapping for the Russian alphabet */ + private static class Rus { + private static final String[] KEY_TO_CHARS = { + "" /* 0 */, + "" /* 1 */, + "АБВГ" /* 2 */, + "ДЕЁЖЗ" /* 3 */, + "ИЙКЛ" /* 4 */, + "МНОП" /* 5 */, + "РСТУ" /* 6 */, + "ФХЦЧ" /* 7 */, + "ШЩЪЫ" /* 8 */, + "ЬЭЮЯ" /* 9 */, + "" /* * */, + "" /* # */, + }; + + private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY = + getCharToKeyMap(KEY_TO_CHARS); + } + + /** The character mapping for the Ukrainian alphabet */ + private static class Ukr { + private static final String[] KEY_TO_CHARS = { + "" /* 0 */, + "" /* 1 */, + "АБВГҐ" /* 2 */, + "ДЕЄЖЗ" /* 3 */, + "ИІЇЙКЛ" /* 4 */, + "МНОП" /* 5 */, + "РСТУ" /* 6 */, + "ФХЦЧ" /* 7 */, + "ШЩ" /* 8 */, + "ЬЮЯ" /* 9 */, + "" /* * */, + "" /* # */, + }; + + private static final SimpleArrayMap<Character, Character> CHAR_TO_KEY = + getCharToKeyMap(KEY_TO_CHARS); + } + + // A map in which each key is an ISO 639-2 language code and the corresponding value is a + // character-key map. + private static final SimpleArrayMap<String, SimpleArrayMap<Character, Character>> + CHAR_TO_KEY_MAPS = new SimpleArrayMap<>(); + + // A map in which each key is an ISO 639-2 language code and the corresponding value is an array + // defining a key-characters map. + private static final SimpleArrayMap<String, String[]> KEY_TO_CHAR_MAPS = new SimpleArrayMap<>(); + + static { + CHAR_TO_KEY_MAPS.put("bul", Bul.CHAR_TO_KEY); + CHAR_TO_KEY_MAPS.put("rus", Rus.CHAR_TO_KEY); + CHAR_TO_KEY_MAPS.put("ukr", Ukr.CHAR_TO_KEY); + + KEY_TO_CHAR_MAPS.put("bul", Bul.KEY_TO_CHARS); + KEY_TO_CHAR_MAPS.put("rus", Rus.KEY_TO_CHARS); + KEY_TO_CHAR_MAPS.put("ukr", Ukr.KEY_TO_CHARS); + } + + /** + * Returns the character-key map of the ISO 639-2 language code of the 1st language preference or + * null if + * + * <ul> + * <li>no character-key map for the language code is defined, or + * <li>the support for dual alphabets is disabled. + * </ul> + */ + public static SimpleArrayMap<Character, Character> getCharToKeyMap(@NonNull Context context) { + return isDualAlphabetsEnabled(context) + ? CHAR_TO_KEY_MAPS.get(CompatUtils.getLocale(context).getISO3Language()) + : null; + } + + /** Returns the default character-key map (the one that uses the Latin alphabet). */ + public static SimpleArrayMap<Character, Character> getDefaultCharToKeyMap() { + return Latin.CHAR_TO_KEY; + } + + /** + * Returns the key-characters map of the given ISO 639-2 language code of the 1st language + * preference or null if + * + * <ul> + * <li>no key-characters map for the language code is defined, or + * <li>the support for dual alphabets is disabled. + * </ul> + */ + public static String[] getKeyToCharsMap(@NonNull Context context) { + return isDualAlphabetsEnabled(context) + ? KEY_TO_CHAR_MAPS.get(CompatUtils.getLocale(context).getISO3Language()) + : null; + } + + /** Returns the default key-characters map (the one that uses the Latin alphabet). */ + public static String[] getDefaultKeyToCharsMap() { + return Latin.KEY_TO_CHARS; + } + + private static boolean isDualAlphabetsEnabled(Context context) { + return ConfigProviderBindings.get(context).getBoolean(FLAG_ENABLE_DUAL_ALPHABETS, false); + } + + /** + * Given a array representing a key-characters map, return its reverse map. + * + * <p>It is the caller's responsibility to ensure that + * + * <ul> + * <li>the array contains only 12 elements, + * <li>the 0th element ~ the 9th element are the mappings for keys "0" ~ "9", + * <li>the 10th element is for key "*", and + * <li>the 11th element is for key "#". + * </ul> + * + * @param keyToChars An array representing a key-characters map. It must satisfy the conditions + * above. + * @return A character-key map. + */ + private static SimpleArrayMap<Character, Character> getCharToKeyMap( + @NonNull String[] keyToChars) { + Assert.checkArgument(keyToChars.length == 12); + + SimpleArrayMap<Character, Character> charToKeyMap = new SimpleArrayMap<>(); + + for (int keyIndex = 0; keyIndex < keyToChars.length; keyIndex++) { + String chars = keyToChars[keyIndex]; + + for (int j = 0; j < chars.length(); j++) { + char c = chars.charAt(j); + if (Character.isAlphabetic(c)) { + charToKeyMap.put(Character.toLowerCase(c), getKeyChar(keyIndex)); + } + } + } + + return charToKeyMap; + } + + /** Given a key index of the dialpad, returns the corresponding character. */ + private static char getKeyChar(int keyIndex) { + Assert.checkArgument(0 <= keyIndex && keyIndex <= 11); + + switch (keyIndex) { + case 10: + return '*'; + case 11: + return '#'; + default: + return (char) ('0' + keyIndex); + } + } +} diff --git a/java/com/android/dialer/dialpadview/DialpadView.java b/java/com/android/dialer/dialpadview/DialpadView.java index 38ab383a8..5794038ce 100644 --- a/java/com/android/dialer/dialpadview/DialpadView.java +++ b/java/com/android/dialer/dialpadview/DialpadView.java @@ -114,9 +114,8 @@ public class DialpadView extends LinearLayout { mIsRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; - mPrimaryLettersMapping = DialpadAlphabets.getDefaultAlphabet(); - mSecondaryLettersMapping = - DialpadAlphabets.getAlphabetForLanguage(CompatUtils.getLocale(context).getISO3Language()); + mPrimaryLettersMapping = DialpadCharMappings.getDefaultKeyToCharsMap(); + mSecondaryLettersMapping = DialpadCharMappings.getKeyToCharsMap(context); } @Override diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java index ba08fe9bf..f85b357e7 100644 --- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java +++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java @@ -19,6 +19,7 @@ package com.android.dialer.phonelookup.composite; import android.support.annotation.NonNull; import android.telecom.Call; import com.android.dialer.DialerPhoneNumber; +import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerFutures; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; @@ -85,9 +86,47 @@ public final class CompositePhoneLookup implements PhoneLookup { futures, Preconditions::checkNotNull, false /* defaultValue */); } + /** + * Delegates to a set of dependent lookups and combines results. + * + * <p>Note: If any of the dependent lookups fails, the returned future will also fail. If any of + * the dependent lookups does not complete, the returned future will also not complete. + */ @Override public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate( ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) { - return null; + List<ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>>> futures = + new ArrayList<>(); + for (PhoneLookup phoneLookup : phoneLookups) { + futures.add(phoneLookup.bulkUpdate(existingInfoMap, lastModified)); + } + return Futures.transform( + Futures.allAsList(futures), + new Function< + List<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>>, + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>>() { + @Override + public ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> apply( + List<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> allMaps) { + ImmutableMap.Builder<DialerPhoneNumber, PhoneLookupInfo> combinedMap = + ImmutableMap.builder(); + for (DialerPhoneNumber dialerPhoneNumber : existingInfoMap.keySet()) { + PhoneLookupInfo.Builder combinedInfo = PhoneLookupInfo.newBuilder(); + for (ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> map : allMaps) { + PhoneLookupInfo subInfo = map.get(dialerPhoneNumber); + if (subInfo == null) { + throw new IllegalStateException( + "A sublookup didn't return an info for number: " + + LogUtil.sanitizePhoneNumber( + dialerPhoneNumber.getRawInput().getNumber())); + } + combinedInfo.mergeFrom(subInfo); + } + combinedMap.put(dialerPhoneNumber, combinedInfo.build()); + } + return combinedMap.build(); + } + }, + MoreExecutors.directExecutor()); } } diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java index f9fc1a6f4..2878e27c4 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java @@ -22,23 +22,47 @@ import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.DeletedContacts; import android.support.annotation.NonNull; +import android.support.v4.util.ArrayMap; import android.support.v4.util.ArraySet; import android.telecom.Call; +import android.text.TextUtils; import com.android.dialer.DialerPhoneNumber; +import com.android.dialer.common.Assert; import com.android.dialer.common.concurrent.DialerExecutors; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; +import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import javax.inject.Inject; /** PhoneLookup implementation for local contacts. */ public final class Cp2PhoneLookup implements PhoneLookup { + private static final String[] CP2_INFO_PROJECTION = + new String[] { + Phone.DISPLAY_NAME_PRIMARY, // 0 + Phone.PHOTO_THUMBNAIL_URI, // 1 + Phone.PHOTO_ID, // 2 + Phone.LABEL, // 3 + Phone.NORMALIZED_NUMBER, // 4 + Phone.CONTACT_ID, // 5 + }; + + private static final int CP2_INFO_NAME_INDEX = 0; + private static final int CP2_INFO_PHOTO_URI_INDEX = 1; + private static final int CP2_INFO_PHOTO_ID_INDEX = 2; + private static final int CP2_INFO_LABEL_INDEX = 3; + private static final int CP2_INFO_NUMBER_INDEX = 4; + private static final int CP2_INFO_CONTACT_ID_INDEX = 5; + private final Context appContext; @Inject @@ -60,12 +84,12 @@ public final class Cp2PhoneLookup implements PhoneLookup { } private boolean isDirtyInternal(ImmutableSet<DialerPhoneNumber> phoneNumbers, long lastModified) { - return contactsUpdated(getContactIdsFromPhoneNumbers(phoneNumbers), lastModified) + return contactsUpdated(queryPhoneTableForContactIds(phoneNumbers), lastModified) || contactsDeleted(lastModified); } /** Returns set of contact ids that correspond to {@code phoneNumbers} if the contact exists. */ - private Set<Long> getContactIdsFromPhoneNumbers(ImmutableSet<DialerPhoneNumber> phoneNumbers) { + private Set<Long> queryPhoneTableForContactIds(ImmutableSet<DialerPhoneNumber> phoneNumbers) { Set<Long> contactIds = new ArraySet<>(); try (Cursor cursor = appContext @@ -73,7 +97,7 @@ public final class Cp2PhoneLookup implements PhoneLookup { .query( Phone.CONTENT_URI, new String[] {Phone.CONTACT_ID}, - columnInSetWhereStatement(Phone.NORMALIZED_NUMBER, phoneNumbers.size()), + Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(phoneNumbers.size()) + ")", contactIdsSelectionArgs(phoneNumbers), null)) { cursor.moveToPosition(-1); @@ -100,37 +124,32 @@ public final class Cp2PhoneLookup implements PhoneLookup { /** Returns true if any contacts were modified after {@code lastModified}. */ private boolean contactsUpdated(Set<Long> contactIds, long lastModified) { - try (Cursor cursor = - appContext - .getContentResolver() - .query( - Contacts.CONTENT_URI, - new String[] {Contacts._ID}, - contactsIsDirtyWhereStatement(contactIds.size()), - contactsIsDirtySelectionArgs(lastModified, contactIds), - null)) { + try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) { return cursor.getCount() > 0; } } - private static String contactsIsDirtyWhereStatement(int numberOfContactIds) { - StringBuilder where = new StringBuilder(); - // Filter to after last modified time - where.append(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP).append(" > ?"); - - // Filter based only on contacts we care about - where.append(" AND ").append(columnInSetWhereStatement(Contacts._ID, numberOfContactIds)); - return where.toString(); - } + private Cursor queryContactsTableForContacts(Set<Long> contactIds, long lastModified) { + // Filter to after last modified time based only on contacts we care about + String where = + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + + " > ?" + + " AND " + + Contacts._ID + + " IN (" + + questionMarks(contactIds.size()) + + ")"; - private String[] contactsIsDirtySelectionArgs(long lastModified, Set<Long> contactIds) { String[] args = new String[contactIds.size() + 1]; args[0] = Long.toString(lastModified); int i = 1; for (Long contactId : contactIds) { args[i++] = Long.toString(contactId); } - return args; + + return appContext + .getContentResolver() + .query(Contacts.CONTENT_URI, new String[] {Contacts._ID}, where, args, null); } /** Returns true if any contacts were deleted after {@code lastModified}. */ @@ -148,22 +167,272 @@ public final class Cp2PhoneLookup implements PhoneLookup { } } - private static String columnInSetWhereStatement(String columnName, int setSize) { + @Override + public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate( + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) { + return MoreExecutors.listeningDecorator(DialerExecutors.getLowPriorityThreadPool(appContext)) + .submit(() -> bulkUpdateInternal(existingInfoMap, lastModified)); + } + + private ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> bulkUpdateInternal( + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) { + // Build a set of each DialerPhoneNumber that was associated with a contact, and is no longer + // associated with that same contact. + Set<DialerPhoneNumber> deletedPhoneNumbers = + getDeletedPhoneNumbers(existingInfoMap, lastModified); + + // For each DialerPhoneNumber that was associated with a contact or added to a contact, + // build a map of those DialerPhoneNumbers to a set Cp2Infos, where each Cp2Info represents a + // contact. + ImmutableMap<DialerPhoneNumber, Set<Cp2Info>> updatedContacts = + buildMapForUpdatedOrAddedContacts(existingInfoMap, lastModified, deletedPhoneNumbers); + + // Start build a new map of updated info. This will replace existing info. + ImmutableMap.Builder<DialerPhoneNumber, PhoneLookupInfo> newInfoMapBuilder = + ImmutableMap.builder(); + + // For each DialerPhoneNumber in existing info... + for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : existingInfoMap.entrySet()) { + // Build off the existing info + PhoneLookupInfo.Builder infoBuilder = PhoneLookupInfo.newBuilder(entry.getValue()); + + // If the contact was updated, replace the Cp2Info list + if (updatedContacts.containsKey(entry.getKey())) { + infoBuilder.clearCp2Info(); + infoBuilder.addAllCp2Info(updatedContacts.get(entry.getKey())); + + // If it was deleted and not added to a new contact, replace the Cp2Info list with + // the default instance of Cp2Info + } else if (deletedPhoneNumbers.contains(entry.getKey())) { + infoBuilder.clearCp2Info(); + infoBuilder.addCp2Info(Cp2Info.getDefaultInstance()); + } + + // If the DialerPhoneNumber didn't change, add the unchanged existing info. + newInfoMapBuilder.put(entry.getKey(), infoBuilder.build()); + } + return newInfoMapBuilder.build(); + } + + /** + * 1. get all contact ids. if the id is unset, add the number to the list of contacts to look up. + * 2. reduce our list of contact ids to those that were updated after lastModified. 3. Now we have + * the smallest set of dialer phone numbers to query cp2 against. 4. build and return the map of + * dialerphonenumbers to their new cp2info + * + * @return Map of {@link DialerPhoneNumber} to {@link PhoneLookupInfo} with updated {@link + * Cp2Info}. + */ + private ImmutableMap<DialerPhoneNumber, Set<Cp2Info>> buildMapForUpdatedOrAddedContacts( + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, + long lastModified, + Set<DialerPhoneNumber> deletedPhoneNumbers) { + + // Start building a set of DialerPhoneNumbers that we want to update. + Set<DialerPhoneNumber> updatedNumbers = new ArraySet<>(); + + Set<Long> contactIds = new ArraySet<>(); + for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : existingInfoMap.entrySet()) { + // If the number was deleted, we need to check if it was added to a new contact. + if (deletedPhoneNumbers.contains(entry.getKey())) { + updatedNumbers.add(entry.getKey()); + continue; + } + + // For each Cp2Info for each existing DialerPhoneNumber... + // Store the contact id if it exist, else automatically add the DialerPhoneNumber to our + // set of DialerPhoneNumbers we want to update. + for (Cp2Info cp2Info : entry.getValue().getCp2InfoList()) { + if (Objects.equals(cp2Info, Cp2Info.getDefaultInstance())) { + // If the number doesn't have any Cp2Info set to it, for various reasons, we need to look + // up the number to check if any exists. + // The various reasons this might happen are: + // - An existing contact that wasn't in the call log is now in the call log. + // - A number was in the call log before but has now been added to a contact. + // - A number is in the call log, but isn't associated with any contact. + updatedNumbers.add(entry.getKey()); + } else { + contactIds.add(cp2Info.getContactId()); + } + } + } + + // Query the contacts table and get those that whose Contacts.CONTACT_LAST_UPDATED_TIMESTAMP is + // after lastModified, such that Contacts._ID is in our set of contact IDs we build above. + try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) { + int contactIdIndex = cursor.getColumnIndex(Contacts._ID); + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + // Find the DialerPhoneNumber for each contact id and add it to our updated numbers set. + // These, along with our number not associated with any Cp2Info need to be updated. + long contactId = cursor.getLong(contactIdIndex); + updatedNumbers.addAll(getDialerPhoneNumber(existingInfoMap, contactId)); + } + } + + // Query the Phone table and build Cp2Info for each DialerPhoneNumber in our updatedNumbers set. + Map<DialerPhoneNumber, Set<Cp2Info>> map = new ArrayMap<>(); + try (Cursor cursor = getAllCp2Rows(updatedNumbers)) { + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + // Map each dialer phone number to it's new cp2 info + Set<DialerPhoneNumber> phoneNumbers = + getDialerPhoneNumbers(updatedNumbers, cursor.getString(CP2_INFO_NUMBER_INDEX)); + Cp2Info info = buildCp2InfoFromUpdatedContactsCursor(cursor); + for (DialerPhoneNumber phoneNumber : phoneNumbers) { + if (map.containsKey(phoneNumber)) { + map.get(phoneNumber).add(info); + } else { + Set<Cp2Info> cp2Infos = new ArraySet<>(); + cp2Infos.add(info); + map.put(phoneNumber, cp2Infos); + } + } + } + } + return ImmutableMap.copyOf(map); + } + + /** + * Returns cursor with projection {@link #CP2_INFO_PROJECTION} and only phone numbers that are in + * {@code updateNumbers}. + */ + private Cursor getAllCp2Rows(Set<DialerPhoneNumber> updatedNumbers) { + String where = Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(updatedNumbers.size()) + ")"; + String[] selectionArgs = new String[updatedNumbers.size()]; + int i = 0; + for (DialerPhoneNumber phoneNumber : updatedNumbers) { + selectionArgs[i++] = getNormalizedNumber(phoneNumber); + } + + return appContext + .getContentResolver() + .query(Phone.CONTENT_URI, CP2_INFO_PROJECTION, where, selectionArgs, null); + } + + /** + * @param cursor with projection {@link #CP2_INFO_PROJECTION}. + * @return new {@link Cp2Info} based on current row of {@code cursor}. + */ + private static Cp2Info buildCp2InfoFromUpdatedContactsCursor(Cursor cursor) { + String displayName = cursor.getString(CP2_INFO_NAME_INDEX); + String photoUri = cursor.getString(CP2_INFO_PHOTO_URI_INDEX); + String label = cursor.getString(CP2_INFO_LABEL_INDEX); + + Cp2Info.Builder infoBuilder = Cp2Info.newBuilder(); + if (!TextUtils.isEmpty(displayName)) { + infoBuilder.setName(displayName); + } + if (!TextUtils.isEmpty(photoUri)) { + infoBuilder.setPhotoUri(photoUri); + } + if (!TextUtils.isEmpty(label)) { + infoBuilder.setLabel(label); + } + infoBuilder.setPhotoId(cursor.getLong(CP2_INFO_PHOTO_ID_INDEX)); + infoBuilder.setContactId(cursor.getLong(CP2_INFO_CONTACT_ID_INDEX)); + return infoBuilder.build(); + } + + /** Returns set of DialerPhoneNumbers that were associated with now deleted contacts. */ + private Set<DialerPhoneNumber> getDeletedPhoneNumbers( + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) { + // Build set of all contact IDs from our existing data. We're going to use this set to query + // against the DeletedContacts table and see if any of them were deleted. + Set<Long> contactIds = findContactIdsIn(existingInfoMap); + + // Start building a set of DialerPhoneNumbers that were associated with now deleted contacts. + try (Cursor cursor = queryDeletedContacts(contactIds, lastModified)) { + // We now have a cursor/list of contact IDs that were associated with deleted contacts. + return findDeletedPhoneNumbersIn(existingInfoMap, cursor); + } + } + + private Set<Long> findContactIdsIn(ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> map) { + Set<Long> contactIds = new ArraySet<>(); + for (PhoneLookupInfo info : map.values()) { + for (Cp2Info cp2Info : info.getCp2InfoList()) { + contactIds.add(cp2Info.getContactId()); + } + } + return contactIds; + } + + private Cursor queryDeletedContacts(Set<Long> contactIds, long lastModified) { + String where = + DeletedContacts.CONTACT_DELETED_TIMESTAMP + + " > ?" + + " AND " + + DeletedContacts.CONTACT_ID + + " IN (" + + questionMarks(contactIds.size()) + + ")"; + String[] args = new String[contactIds.size() + 1]; + args[0] = Long.toString(lastModified); + int i = 1; + for (Long contactId : contactIds) { + args[i++] = Long.toString(contactId); + } + + return appContext + .getContentResolver() + .query( + DeletedContacts.CONTENT_URI, + new String[] {DeletedContacts.CONTACT_ID}, + where, + args, + null); + } + + /** Returns set of DialerPhoneNumbers that are associated with deleted contact IDs. */ + private Set<DialerPhoneNumber> findDeletedPhoneNumbersIn( + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, Cursor cursor) { + int contactIdIndex = cursor.getColumnIndexOrThrow(DeletedContacts.CONTACT_ID); + Set<DialerPhoneNumber> deletedPhoneNumbers = new ArraySet<>(); + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + long contactId = cursor.getLong(contactIdIndex); + deletedPhoneNumbers.addAll(getDialerPhoneNumber(existingInfoMap, contactId)); + } + return deletedPhoneNumbers; + } + + private static Set<DialerPhoneNumber> getDialerPhoneNumbers( + Set<DialerPhoneNumber> phoneNumbers, String number) { + Set<DialerPhoneNumber> matches = new ArraySet<>(); + for (DialerPhoneNumber phoneNumber : phoneNumbers) { + if (getNormalizedNumber(phoneNumber).equals(number)) { + matches.add(phoneNumber); + } + } + Assert.checkArgument( + matches.size() > 0, "Couldn't find DialerPhoneNumber for number: " + number); + return matches; + } + + private static Set<DialerPhoneNumber> getDialerPhoneNumber( + ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long contactId) { + Set<DialerPhoneNumber> matches = new ArraySet<>(); + for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : existingInfoMap.entrySet()) { + for (Cp2Info cp2Info : entry.getValue().getCp2InfoList()) { + if (cp2Info.getContactId() == contactId) { + matches.add(entry.getKey()); + } + } + } + Assert.checkArgument( + matches.size() > 0, "Couldn't find DialerPhoneNumber for contact ID: " + contactId); + return matches; + } + + private static String questionMarks(int count) { StringBuilder where = new StringBuilder(); - where.append(columnName).append(" IN ("); - for (int i = 0; i < setSize; i++) { + for (int i = 0; i < count; i++) { if (i != 0) { where.append(", "); } where.append("?"); } - return where.append(")").toString(); - } - - @Override - public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate( - ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) { - // TODO(calderwoodra) - return null; + return where.toString(); } } diff --git a/java/com/android/dialer/phonelookup/phone_lookup_info.proto b/java/com/android/dialer/phonelookup/phone_lookup_info.proto index 1027e5c22..cb89a64e3 100644 --- a/java/com/android/dialer/phonelookup/phone_lookup_info.proto +++ b/java/com/android/dialer/phonelookup/phone_lookup_info.proto @@ -17,10 +17,23 @@ message PhoneLookupInfo { // Information about a PhoneNumber retrieved from CP2. Cp2PhoneLookup is // responsible for populating the data in this message. message Cp2Info { + // android.provider.ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_PRIMARY optional string name = 1; + + // android.provider.ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI optional string photo_uri = 2; + + // android.provider.ContactsContract.CommonDataKinds.Phone.PHOTO_ID optional fixed64 photo_id = 3; - optional string label = 4; // "Home", "Mobile", ect. + + // android.provider.ContactsContract.CommonDataKinds.Phone.LABEL + // "Home", "Mobile", ect. + optional string label = 4; + + // android.provider.ContactsContract.CommonDataKinds.Phone.CONTACT_ID + optional fixed64 contact_id = 5; } - optional Cp2Info cp2_info = 1; + // Repeated because one phone number can be associated with multiple CP2 + // contacts. + repeated Cp2Info cp2_info = 1; }
\ No newline at end of file diff --git a/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java b/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java index 4413252f4..9ac6e7c5e 100644 --- a/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java +++ b/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java @@ -16,6 +16,7 @@ package com.android.dialer.searchfragment.common; +import android.content.Context; import android.graphics.Typeface; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -49,14 +50,16 @@ public class QueryBoldingUtil { * * @param query containing any characters * @param name of a contact/string that query will compare to + * @param context of the app * @return name with query bolded if query can be found in the name. */ - public static CharSequence getNameWithQueryBolded(@Nullable String query, @NonNull String name) { + public static CharSequence getNameWithQueryBolded( + @Nullable String query, @NonNull String name, @NonNull Context context) { if (TextUtils.isEmpty(query)) { return name; } - if (!QueryFilteringUtil.nameMatchesT9Query(query, name)) { + if (!QueryFilteringUtil.nameMatchesT9Query(query, name, context)) { Pattern pattern = Pattern.compile("(^|\\s)" + Pattern.quote(query.toLowerCase())); Matcher matcher = pattern.matcher(name.toLowerCase()); if (matcher.find()) { @@ -69,7 +72,7 @@ public class QueryBoldingUtil { } Pattern pattern = Pattern.compile("(^|\\s)" + Pattern.quote(query.toLowerCase())); - Matcher matcher = pattern.matcher(QueryFilteringUtil.getT9Representation(name)); + Matcher matcher = pattern.matcher(QueryFilteringUtil.getT9Representation(name, context)); if (matcher.find()) { // query matches the start of a T9 name (i.e. 75 -> "Jessica [Jo]nes") int index = matcher.start(); @@ -79,11 +82,12 @@ public class QueryBoldingUtil { } else { // query match the T9 initials (i.e. 222 -> "[A]l [B]ob [C]harlie") - return getNameWithInitialsBolded(query, name); + return getNameWithInitialsBolded(query, name, context); } } - private static CharSequence getNameWithInitialsBolded(String query, String name) { + private static CharSequence getNameWithInitialsBolded( + String query, String name, Context context) { SpannableString boldedInitials = new SpannableString(name); name = name.toLowerCase(); int initialsBolded = 0; @@ -91,7 +95,8 @@ public class QueryBoldingUtil { while (++nameIndex < name.length() && initialsBolded < query.length()) { if ((nameIndex == 0 || name.charAt(nameIndex - 1) == ' ') - && QueryFilteringUtil.getDigit(name.charAt(nameIndex)) == query.charAt(initialsBolded)) { + && QueryFilteringUtil.getDigit(name.charAt(nameIndex), context) + == query.charAt(initialsBolded)) { boldedInitials.setSpan( new StyleSpan(Typeface.BOLD), nameIndex, diff --git a/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java b/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java index 6b5cea88d..1ecb486d2 100644 --- a/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java +++ b/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java @@ -16,14 +16,24 @@ package com.android.dialer.searchfragment.common; +import android.content.Context; import android.support.annotation.NonNull; +import android.support.v4.util.SimpleArrayMap; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import com.android.dialer.dialpadview.DialpadCharMappings; import java.util.regex.Pattern; /** Utility class for filtering, comparing and handling strings and queries. */ public class QueryFilteringUtil { + /** + * The default character-digit map that will be used to find the digit associated with a given + * character on a T9 keyboard. + */ + private static final SimpleArrayMap<Character, Character> DEFAULT_CHAR_TO_DIGIT_MAP = + DialpadCharMappings.getDefaultCharToKeyMap(); + /** Matches strings with "-", "(", ")", 2-9 of at least length one. */ private static final Pattern T9_PATTERN = Pattern.compile("[\\-()2-9]+"); @@ -38,15 +48,29 @@ public class QueryFilteringUtil { * <li>#nameMatchesT9Query("56", "Jessica Jones") returns true, 56 -> 'Jo' * <li>#nameMatchesT9Query("7", "Jessica Jones") returns false, no names start with P,Q,R or S * </ul> + * + * <p>When the 1st language preference uses a non-Latin alphabet (e.g., Russian) and the character + * mappings for the alphabet is defined in {@link DialpadCharMappings}, the Latin alphabet will be + * used first to check if the name matches the query. If they don't match, the non-Latin alphabet + * will be used. + * + * <p>Examples (when the 1st language preference is Russian): + * + * <ul> + * <li>#nameMatchesT9Query("7", "John Smith") returns true, 7 -> 'S' + * <li>#nameMatchesT9Query("7", "Павел Чехов") returns true, 7 -> 'Ч' + * <li>#nameMatchesT9Query("77", "Pavel Чехов") returns true, 7 -> 'P' (in the Latin alphabet), + * 7 -> 'Ч' (in the Russian alphabet) + * </ul> */ - public static boolean nameMatchesT9Query(String query, String name) { + public static boolean nameMatchesT9Query(String query, String name, Context context) { if (!T9_PATTERN.matcher(query).matches()) { return false; } query = digitsOnly(query); Pattern pattern = Pattern.compile("(^|\\s)" + Pattern.quote(query)); - if (pattern.matcher(getT9Representation(name)).find()) { + if (pattern.matcher(getT9Representation(name, context)).find()) { // query matches the start of a T9 name (i.e. 75 -> "Jessica [Jo]nes") return true; } @@ -61,7 +85,7 @@ public class QueryFilteringUtil { continue; } - if (getDigit(names[i].charAt(0)) == query.charAt(queryIndex)) { + if (getDigit(names[i].charAt(0), context) == query.charAt(queryIndex)) { queryIndex++; } } @@ -106,11 +130,17 @@ public class QueryFilteringUtil { return digitsOnly(number).indexOf(digitsOnly(query)); } - // Returns string with letters replaced with their T9 representation. - static String getT9Representation(String s) { + /** + * Replaces characters in the given string with their T9 representations. + * + * @param s The original string + * @param context The context + * @return The original string with characters replaced with T9 representations. + */ + static String getT9Representation(String s, Context context) { StringBuilder builder = new StringBuilder(s.length()); for (char c : s.toLowerCase().toCharArray()) { - builder.append(getDigit(c)); + builder.append(getDigit(c, context)); } return builder.toString(); } @@ -127,45 +157,26 @@ public class QueryFilteringUtil { return sb.toString(); } - // Returns the T9 representation of a lower case character, otherwise returns the character. - static char getDigit(char c) { - switch (c) { - case 'a': - case 'b': - case 'c': - return '2'; - case 'd': - case 'e': - case 'f': - return '3'; - case 'g': - case 'h': - case 'i': - return '4'; - case 'j': - case 'k': - case 'l': - return '5'; - case 'm': - case 'n': - case 'o': - return '6'; - case 'p': - case 'q': - case 'r': - case 's': - return '7'; - case 't': - case 'u': - case 'v': - return '8'; - case 'w': - case 'x': - case 'y': - case 'z': - return '9'; - default: - return c; + /** + * Returns the digit on a T9 keyboard which is associated with the given lower case character. + * + * <p>The default character-key mapping will be used first to find a digit. If no digit is found, + * try the mapping of the current default locale if it is defined in {@link DialpadCharMappings}. + * If the second attempt fails, return the original character. + */ + static char getDigit(char c, Context context) { + Character digit = DEFAULT_CHAR_TO_DIGIT_MAP.get(c); + if (digit != null) { + return digit; + } + + SimpleArrayMap<Character, Character> charToKeyMap = + DialpadCharMappings.getCharToKeyMap(context); + if (charToKeyMap != null) { + digit = charToKeyMap.get(c); + return digit != null ? digit : c; } + + return c; } } diff --git a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java index 84c22a2cf..df67b762f 100644 --- a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java +++ b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java @@ -17,6 +17,7 @@ package com.android.dialer.searchfragment.cp2; import android.content.ContentResolver; +import android.content.Context; import android.database.CharArrayBuffer; import android.database.ContentObserver; import android.database.Cursor; @@ -44,7 +45,7 @@ import java.util.Set; * Wrapper for a cursor containing all on device contacts. * * <p>This cursor removes duplicate phone numbers associated with the same contact and can filter - * contacts based on a query by calling {@link #filter(String)}. + * contacts based on a query by calling {@link #filter(String, Context)}. */ final class ContactFilterCursor implements Cursor { @@ -72,10 +73,11 @@ final class ContactFilterCursor implements Cursor { /** * @param cursor with projection {@link Projections#CP2_PROJECTION}. * @param query to filter cursor results. + * @param context of the app. */ - ContactFilterCursor(Cursor cursor, @Nullable String query) { + ContactFilterCursor(Cursor cursor, @Nullable String query, Context context) { this.cursor = createCursor(cursor); - filter(query); + filter(query, context); } /** @@ -238,7 +240,7 @@ final class ContactFilterCursor implements Cursor { * <li>Its company contains the query * </ul> */ - public void filter(@Nullable String query) { + public void filter(@Nullable String query, Context context) { if (query == null) { query = ""; } @@ -253,7 +255,7 @@ final class ContactFilterCursor implements Cursor { String companyName = cursor.getString(Projections.COMPANY_NAME); String nickName = cursor.getString(Projections.NICKNAME); if (TextUtils.isEmpty(query) - || QueryFilteringUtil.nameMatchesT9Query(query, name) + || QueryFilteringUtil.nameMatchesT9Query(query, name, context) || QueryFilteringUtil.numberMatchesNumberQuery(query, number) || QueryFilteringUtil.nameContainsQuery(query, name) || QueryFilteringUtil.nameContainsQuery(query, companyName) diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java index c09396c72..386ab3a6b 100644 --- a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java +++ b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java @@ -104,7 +104,7 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick : context.getString( com.android.contacts.common.R.string.call_subject_type_and_number, label, number); - nameOrNumberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name)); + nameOrNumberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context)); numberView.setText(QueryBoldingUtil.getNumberWithQueryBolded(query, secondaryInfo)); setCallToAction(cursor, query); diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursor.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursor.java index 508ca7f57..7697e0520 100644 --- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursor.java +++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursor.java @@ -32,17 +32,19 @@ import com.android.dialer.searchfragment.common.SearchCursor; final class SearchContactsCursor extends MergeCursor implements SearchCursor { private final ContactFilterCursor contactFilterCursor; + private final Context context; static SearchContactsCursor newInstance( Context context, ContactFilterCursor contactFilterCursor) { MatrixCursor headerCursor = new MatrixCursor(HEADER_PROJECTION); headerCursor.addRow(new String[] {context.getString(R.string.all_contacts)}); - return new SearchContactsCursor(new Cursor[] {headerCursor, contactFilterCursor}); + return new SearchContactsCursor(new Cursor[] {headerCursor, contactFilterCursor}, context); } - private SearchContactsCursor(Cursor[] cursors) { + private SearchContactsCursor(Cursor[] cursors, Context context) { super(cursors); - contactFilterCursor = (ContactFilterCursor) cursors[1]; + this.contactFilterCursor = (ContactFilterCursor) cursors[1]; + this.context = context; } @Override @@ -52,7 +54,7 @@ final class SearchContactsCursor extends MergeCursor implements SearchCursor { @Override public boolean updateQuery(@Nullable String query) { - contactFilterCursor.filter(query); + contactFilterCursor.filter(query, context); return true; } diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java index d3abbffca..35518019e 100644 --- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java +++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java @@ -61,7 +61,7 @@ public final class SearchContactsCursorLoader extends CursorLoader { // All contacts Cursor cursor = super.loadInBackground(); // Filtering logic - ContactFilterCursor contactFilterCursor = new ContactFilterCursor(cursor, query); + ContactFilterCursor contactFilterCursor = new ContactFilterCursor(cursor, query, getContext()); // Header logic return SearchContactsCursor.newInstance(getContext(), contactFilterCursor); } diff --git a/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlaceViewHolder.java b/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlaceViewHolder.java index 5d5188059..2e1fd5e9d 100644 --- a/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlaceViewHolder.java +++ b/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlaceViewHolder.java @@ -64,8 +64,8 @@ public final class NearbyPlaceViewHolder extends RecyclerView.ViewHolder String name = cursor.getString(Projections.DISPLAY_NAME); String address = cursor.getString(Projections.PHONE_LABEL); - placeName.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name)); - placeAddress.setText(QueryBoldingUtil.getNameWithQueryBolded(query, address)); + placeName.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context)); + placeAddress.setText(QueryBoldingUtil.getNameWithQueryBolded(query, address, context)); String photoUri = cursor.getString(Projections.PHOTO_URI); ContactPhotoManager.getInstance(context) .loadDialerThumbnailOrPhoto( diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java b/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java index 8a02eb9b9..339855fbb 100644 --- a/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java +++ b/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java @@ -72,8 +72,8 @@ public final class RemoteContactViewHolder extends RecyclerView.ViewHolder : context.getString( com.android.contacts.common.R.string.call_subject_type_and_number, label, number); - nameView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name)); - numberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, secondaryInfo)); + nameView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context)); + numberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, secondaryInfo, context)); if (shouldShowPhoto(cursor)) { nameView.setVisibility(View.VISIBLE); diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java index fc31c74e2..73b414d46 100644 --- a/java/com/android/incallui/incall/impl/InCallFragment.java +++ b/java/com/android/incallui/incall/impl/InCallFragment.java @@ -320,7 +320,7 @@ public class InCallFragment extends Fragment } } transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top); - transaction.commitAllowingStateLoss(); + transaction.commitNowAllowingStateLoss(); } @Override diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java index f4996a097..98c8461f5 100644 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java @@ -80,6 +80,11 @@ public class TranscriptionConfigProvider { .getBoolean("voicemail_transcription_donation_available", false); } + public boolean useClientGeneratedVoicemailIds() { + return ConfigProviderBindings.get(context) + .getBoolean("voicemail_transcription_client_generated_voicemail_ids", false); + } + @Override public String toString() { return String.format( diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java index f946607b5..808bf0f87 100644 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java @@ -17,6 +17,7 @@ package com.android.voicemail.impl.transcribe; import android.app.job.JobWorkItem; import android.content.Context; +import android.support.annotation.VisibleForTesting; import android.util.Pair; import com.android.dialer.common.Assert; import com.android.dialer.logging.DialerImpression; @@ -121,13 +122,21 @@ public class TranscriptionTaskAsync extends TranscriptionTask { return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY); } + @VisibleForTesting TranscribeVoicemailAsyncRequest getUploadRequest() { - return TranscribeVoicemailAsyncRequest.newBuilder() - .setVoicemailData(audioData) - .setAudioFormat(encoding) - .setDonationPreference( - isDonationEnabled() ? DonationPreference.DONATE : DonationPreference.DO_NOT_DONATE) - .build(); + TranscribeVoicemailAsyncRequest.Builder builder = + TranscribeVoicemailAsyncRequest.newBuilder() + .setVoicemailData(audioData) + .setAudioFormat(encoding) + .setDonationPreference( + isDonationEnabled() ? DonationPreference.DONATE : DonationPreference.DO_NOT_DONATE); + // Generate the transcript id locally if configured to do so, or if voicemail donation is + // available (because rating donating voicemails requires locally generated voicemail ids). + if (configProvider.useClientGeneratedVoicemailIds() + || configProvider.isVoicemailDonationAvailable()) { + builder.setTranscriptionId(TranscriptionUtils.getFingerprintFor(audioData)); + } + return builder.build(); } private boolean isDonationEnabled() { diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java b/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java new file mode 100644 index 000000000..a001f179a --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java @@ -0,0 +1,41 @@ +/* + * 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.voicemail.impl.transcribe; + +import android.annotation.TargetApi; +import android.os.Build.VERSION_CODES; +import android.util.Base64; +import com.android.dialer.common.Assert; +import com.google.protobuf.ByteString; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** Utility methods used by this transcription package. */ +public class TranscriptionUtils { + + @TargetApi(VERSION_CODES.O) + static String getFingerprintFor(ByteString data) { + Assert.checkArgument(data != null); + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] md5Bytes = md.digest(data.toByteArray()); + return Base64.encodeToString(md5Bytes, Base64.DEFAULT); + } catch (NoSuchAlgorithmException e) { + Assert.fail(e.toString()); + } + return null; + } +} |