summaryrefslogtreecommitdiff
path: root/java/com/android
diff options
context:
space:
mode:
authorTreehugger Robot <treehugger-gerrit@google.com>2017-11-11 08:43:20 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2017-11-11 08:43:20 +0000
commitbed757cd19dca5dffa2a07af579f65028419c242 (patch)
tree6c865335b8872cc084e5a30993d75fcd257c005e /java/com/android
parent50a0f00319c6fe9730d42b1b5f8b90d49b7a140a (diff)
parent60dd96aa640ed4619de915576b5ed0df53081fc4 (diff)
Merge changes I0115fb26,I7a29bf5a,I7888f3b6,I06d5d3d8,I4c601b16, ...
* changes: Implemented CompositePhoneLookup#bulkUpdate. Allow the client to specify voicemail transcript id's Implement bulk update for Cp2PhoneLookup. Add character mappings for Bulgarian & Ukrainian in DialpadCharMappings. Support dual alphabets in smart search when a secondary alphabet is available. Commit transaction synchronously for OnHoldFragment.
Diffstat (limited to 'java/com/android')
-rw-r--r--java/com/android/dialer/dialpadview/DialpadAlphabets.java80
-rw-r--r--java/com/android/dialer/dialpadview/DialpadCharMappings.java228
-rw-r--r--java/com/android/dialer/dialpadview/DialpadView.java5
-rw-r--r--java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java41
-rw-r--r--java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java337
-rw-r--r--java/com/android/dialer/phonelookup/phone_lookup_info.proto17
-rw-r--r--java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java17
-rw-r--r--java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java101
-rw-r--r--java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java12
-rw-r--r--java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java2
-rw-r--r--java/com/android/dialer/searchfragment/cp2/SearchContactsCursor.java10
-rw-r--r--java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java2
-rw-r--r--java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlaceViewHolder.java4
-rw-r--r--java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java4
-rw-r--r--java/com/android/incallui/incall/impl/InCallFragment.java2
-rw-r--r--java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java5
-rw-r--r--java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java21
-rw-r--r--java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java41
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;
+ }
+}