From c35c7580d48516eb8fedaad1142a40428c780946 Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Tue, 19 Dec 2017 15:58:58 -0800 Subject: Implemented SpeedDialEntry Room Database. This CL includes everything needed to interface with the SpeedDialEntry database. As an excersice to demonstrate it's functionlity, it also updates favorites to read from SpeedDialEntry database and the add favorites screen to insert into it. Bug: 36841782 Test: existing PiperOrigin-RevId: 179615699 Change-Id: I8938ef87244dffbd240506c112b445d147a88193 --- .../binary/aosp/AospDialerRootComponent.java | 2 + .../basecomponent/BaseDialerRootComponent.java | 4 +- .../google/GoogleStubDialerRootComponent.java | 2 + .../databasepopulator/ContactsPopulator.java | 21 ++++++ .../dialer/speeddial/AddFavoriteActivity.java | 12 +++- .../android/dialer/speeddial/DisambigDialog.java | 2 + .../dialer/speeddial/FavoritesViewHolder.java | 2 +- .../android/dialer/speeddial/SpeedDialAdapter.java | 2 +- .../android/dialer/speeddial/SpeedDialCursor.java | 77 ++++++++++++++-------- .../speeddial/StrequentContactsCursorLoader.java | 39 ++++++----- .../speeddial/room/SpeedDialDatabaseComponent.java | 38 +++++++++++ .../speeddial/room/SpeedDialDatabaseModule.java | 35 ++++++++++ .../dialer/speeddial/room/SpeedDialEntry.java | 58 ++++++++++++++++ .../dialer/speeddial/room/SpeedDialEntryDao.java | 44 +++++++++++++ .../speeddial/room/SpeedDialEntryDatabase.java | 55 ++++++++++++++++ 15 files changed, 345 insertions(+), 48 deletions(-) create mode 100644 java/com/android/dialer/speeddial/room/SpeedDialDatabaseComponent.java create mode 100644 java/com/android/dialer/speeddial/room/SpeedDialDatabaseModule.java create mode 100644 java/com/android/dialer/speeddial/room/SpeedDialEntry.java create mode 100644 java/com/android/dialer/speeddial/room/SpeedDialEntryDao.java create mode 100644 java/com/android/dialer/speeddial/room/SpeedDialEntryDatabase.java (limited to 'java/com/android') diff --git a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java index c344fad62..0e74eff78 100644 --- a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java +++ b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java @@ -30,6 +30,7 @@ import com.android.dialer.precall.impl.PreCallModule; import com.android.dialer.preferredsim.suggestion.stub.StubSimSuggestionModule; import com.android.dialer.simulator.impl.SimulatorModule; import com.android.dialer.spam.StubSpamModule; +import com.android.dialer.speeddial.room.SpeedDialDatabaseModule; import com.android.dialer.storage.StorageModule; import com.android.dialer.strictmode.impl.SystemStrictModeModule; import com.android.incallui.calllocation.stub.StubCallLocationModule; @@ -60,6 +61,7 @@ import javax.inject.Singleton; StubSimSuggestionModule.class, StubFeedbackModule.class, StubSpamModule.class, + SpeedDialDatabaseModule.class, } ) public interface AospDialerRootComponent extends BaseDialerRootComponent {} diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java index 8973a329f..66bd04aa0 100644 --- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java +++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java @@ -30,6 +30,7 @@ import com.android.dialer.precall.PreCallComponent; import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent; import com.android.dialer.simulator.SimulatorComponent; import com.android.dialer.spam.SpamComponent; +import com.android.dialer.speeddial.room.SpeedDialDatabaseComponent; import com.android.dialer.storage.StorageComponent; import com.android.dialer.strictmode.StrictModeComponent; import com.android.incallui.calllocation.CallLocationComponent; @@ -59,4 +60,5 @@ public interface BaseDialerRootComponent StorageComponent.HasComponent, StrictModeComponent.HasComponent, VoicemailComponent.HasComponent, - SpamComponent.HasComponent {} + SpamComponent.HasComponent, + SpeedDialDatabaseComponent.HasComponent {} diff --git a/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java b/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java index b54e06229..891e477c9 100644 --- a/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java +++ b/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java @@ -30,6 +30,7 @@ import com.android.dialer.precall.impl.PreCallModule; import com.android.dialer.preferredsim.suggestion.stub.StubSimSuggestionModule; import com.android.dialer.simulator.impl.SimulatorModule; import com.android.dialer.spam.StubSpamModule; +import com.android.dialer.speeddial.room.SpeedDialDatabaseModule; import com.android.dialer.storage.StorageModule; import com.android.dialer.strictmode.impl.SystemStrictModeModule; import com.android.incallui.calllocation.impl.CallLocationModule; @@ -55,6 +56,7 @@ import javax.inject.Singleton; StubSimSuggestionModule.class, SharedPrefConfigProviderModule.class, SimulatorModule.class, + SpeedDialDatabaseModule.class, StorageModule.class, SystemStrictModeModule.class, StubEnrichedCallModule.class, diff --git a/java/com/android/dialer/databasepopulator/ContactsPopulator.java b/java/com/android/dialer/databasepopulator/ContactsPopulator.java index f22552db7..484851826 100644 --- a/java/com/android/dialer/databasepopulator/ContactsPopulator.java +++ b/java/com/android/dialer/databasepopulator/ContactsPopulator.java @@ -32,6 +32,8 @@ import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.text.TextUtils; import com.android.dialer.common.Assert; +import com.android.dialer.speeddial.room.SpeedDialDatabaseComponent; +import com.android.dialer.speeddial.room.SpeedDialEntry; import com.google.auto.value.AutoValue; import java.io.ByteArrayOutputStream; import java.util.ArrayList; @@ -49,6 +51,7 @@ public final class ContactsPopulator { .addEmail(new Email("m@example.com")) .setIsStarred(true) .setPinned(1) + .setContactId(1L) .setOrangePhoto() .build(), // US, contact with a non-e164 number. @@ -58,6 +61,7 @@ public final class ContactsPopulator { .addEmail(new Email("l@example.com")) .setIsStarred(true) .setPinned(2) + .setContactId(2L) .setBluePhoto() .build(), // UK, number where the (0) should be dropped. @@ -67,6 +71,7 @@ public final class ContactsPopulator { .addEmail(new Email("r@example.com")) .setIsStarred(true) .setPinned(3) + .setContactId(3L) .setRedPhoto() .build(), // US and Australia, contact with a long name and multiple phone numbers. @@ -77,6 +82,7 @@ public final class ContactsPopulator { .addPhoneNumber(new PhoneNumber("+61 2 9374 4001", Phone.TYPE_FAX_HOME)) .setIsStarred(true) .setPinned(4) + .setContactId(4L) .setPurplePhoto() .build(), // US, phone number shared with another contact and 2nd phone number with wait and pause. @@ -155,6 +161,14 @@ public final class ContactsPopulator { addContact(SIMPLE_CONTACTS[5], operations); try { context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations); + SpeedDialDatabaseComponent.get(context.getApplicationContext()) + .speedDialDatabase() + .getSpeedDialEntryDao() + .insert( + new SpeedDialEntry( + SIMPLE_CONTACTS[0].getPhoneNumbers().get(0).value, + SIMPLE_CONTACTS[0].getContactId(), + SpeedDialEntry.VOICE)); } catch (RemoteException | OperationApplicationException e) { Assert.fail("error adding contacts: " + e); } @@ -192,6 +206,7 @@ public final class ContactsPopulator { .withValue( ContactsContract.RawContacts.PINNED, contact.getIsStarred() ? contact.getPinned() : 0) + .withValue(ContactsContract.RawContacts.CONTACT_ID, contact.getContactId()) .withYieldAllowed(true) .build()); @@ -262,6 +277,9 @@ public final class ContactsPopulator { abstract int getPinned(); + @NonNull + abstract long getContactId(); + @Nullable abstract ByteArrayOutputStream getPhotoStream(); @@ -277,6 +295,7 @@ public final class ContactsPopulator { .setAccountName("foo@example") .setPinned(0) .setIsStarred(false) + .setContactId(0) .setPhoneNumbers(new ArrayList<>()) .setEmails(new ArrayList<>()); } @@ -296,6 +315,8 @@ public final class ContactsPopulator { abstract Builder setPinned(int position); + abstract Builder setContactId(long contactId); + abstract Builder setPhotoStream(ByteArrayOutputStream photoStream); abstract Builder setPhoneNumbers(@NonNull List phoneNumbers); diff --git a/java/com/android/dialer/speeddial/AddFavoriteActivity.java b/java/com/android/dialer/speeddial/AddFavoriteActivity.java index eea6e840e..93cd536df 100644 --- a/java/com/android/dialer/speeddial/AddFavoriteActivity.java +++ b/java/com/android/dialer/speeddial/AddFavoriteActivity.java @@ -31,6 +31,8 @@ import com.android.dialer.common.Assert; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.contactsfragment.ContactsFragment; import com.android.dialer.contactsfragment.ContactsFragment.OnContactSelectedListener; +import com.android.dialer.speeddial.room.SpeedDialDatabaseComponent; +import com.android.dialer.speeddial.room.SpeedDialEntry; /** * Activity for selecting a single contact and adding it to favorites. @@ -93,13 +95,19 @@ public class AddFavoriteActivity extends AppCompatActivity implements OnContactS @WorkerThread private int markContactStarred(long contactId) { - // TODO(calderwoodra): For now, we will just mark contacts as starred. This means that contacts - // will only be able to exist once in favorites until we implement multiple contact avenues. + // Insert into SpeedDialEntry database + SpeedDialDatabaseComponent.get(getApplicationContext()) + .speedDialDatabase() + .getSpeedDialEntryDao() + .insert(SpeedDialEntry.newSpeedDialEntry(contactId)); + + // Insert into Cp2 ContentValues contentValues = new ContentValues(); contentValues.put(Contacts.STARRED, 1); String where = Contacts._ID + " = ?"; String[] selectionArgs = new String[] {Long.toString(contactId)}; + return getContentResolver().update(Contacts.CONTENT_URI, contentValues, where, selectionArgs); } diff --git a/java/com/android/dialer/speeddial/DisambigDialog.java b/java/com/android/dialer/speeddial/DisambigDialog.java index ca02f41eb..5598d1e48 100644 --- a/java/com/android/dialer/speeddial/DisambigDialog.java +++ b/java/com/android/dialer/speeddial/DisambigDialog.java @@ -234,6 +234,8 @@ public class DisambigDialog extends DialogFragment { ArrayList projection = new ArrayList<>(Arrays.asList(LookupContactInfoWorker.projection)); projection.add(Phone.LOOKUP_KEY); + projection.add(Phone.CONTACT_ID); + projection.add(Phone.STARRED); return projection.toArray(new String[projection.size()]); } diff --git a/java/com/android/dialer/speeddial/FavoritesViewHolder.java b/java/com/android/dialer/speeddial/FavoritesViewHolder.java index c25b05ead..f9eec8e79 100644 --- a/java/com/android/dialer/speeddial/FavoritesViewHolder.java +++ b/java/com/android/dialer/speeddial/FavoritesViewHolder.java @@ -122,7 +122,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder public interface FavoriteContactsListener { /** Called when the user clicks on a favorite contact that doesn't have a default number. */ - void onAmbiguousContactClicked(String contactId); + void onAmbiguousContactClicked(String lookupKey); /** Called when the user clicks on a favorite contact. */ void onClick(String number, boolean isVideoCall); diff --git a/java/com/android/dialer/speeddial/SpeedDialAdapter.java b/java/com/android/dialer/speeddial/SpeedDialAdapter.java index 5f7b68e5c..b72647550 100644 --- a/java/com/android/dialer/speeddial/SpeedDialAdapter.java +++ b/java/com/android/dialer/speeddial/SpeedDialAdapter.java @@ -90,7 +90,7 @@ final class SpeedDialAdapter extends RecyclerView.Adapter entries) { if (strequentCursor == null || strequentCursor.getCount() == 0) { return null; } - SpeedDialCursor cursor = new SpeedDialCursor(buildCursors(strequentCursor)); + SpeedDialCursor cursor = new SpeedDialCursor(buildCursors(strequentCursor, entries)); strequentCursor.close(); return cursor; } - private static Cursor[] buildCursors(Cursor strequentCursor) { + private static Cursor[] buildCursors(Cursor strequentCursor, List entries) { MatrixCursor starred = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); MatrixCursor suggestions = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); - strequentCursor.moveToPosition(-1); - while (strequentCursor.moveToNext()) { - if (strequentCursor.getPosition() != 0) { - long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID); - int position = strequentCursor.getPosition(); - boolean duplicate = false; - // Iterate backwards through the cursor to check that this isn't a duplicate contact - // TODO(calderwoodra): improve this algorithm (currently O(n^2)). - while (strequentCursor.moveToPrevious() && !duplicate) { - duplicate |= - strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID) == contactId; - } - strequentCursor.moveToPosition(position); - if (duplicate) { - continue; - } + // Build starred cursor + for (SpeedDialEntry entry : entries) { + if (!moveToContactEntry(strequentCursor, entry)) { + LogUtil.e("SpeedDialCursor.buildCursors", "Entry not found: " + entry); + continue; } - if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { - StrequentContactsCursorLoader.addToCursor(starred, strequentCursor); - } else if (starred.getCount() + suggestions.getCount() < SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT) { - // Since all starred contacts come before each non-starred contact, it's safe to assume that - // this list will never exceed the soft limit unless there are more starred contacts than - // the limit permits. - StrequentContactsCursorLoader.addToCursor(suggestions, strequentCursor); + if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) != 1) { + LogUtil.e("SpeedDialCursor.buildCursors", "SpeedDialEntry contact is no longer starred"); + continue; } + StrequentContactsCursorLoader.addToCursor(starred, strequentCursor); } + // Build suggestions cursor + strequentCursor.moveToFirst(); + Set contactIds = new ArraySet<>(); + do { + if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { + // Starred contact + continue; + } + + long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID); + if (!contactIds.add(contactId)) { + // duplicate contact + continue; + } + + StrequentContactsCursorLoader.addToCursor(suggestions, strequentCursor); + } while (strequentCursor.moveToNext() + && starred.getCount() + suggestions.getCount() < SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT); + List cursorList = new ArrayList<>(); + cursorList.add(createHeaderCursor(R.string.favorites_header)); if (starred.getCount() > 0) { - cursorList.add(createHeaderCursor(R.string.favorites_header)); cursorList.add(starred); } if (suggestions.getCount() > 0) { @@ -102,6 +112,17 @@ final class SpeedDialCursor extends MergeCursor { return cursorList.toArray(new Cursor[cursorList.size()]); } + private static boolean moveToContactEntry(Cursor strequentCursor, SpeedDialEntry entry) { + boolean matchFound; + strequentCursor.moveToFirst(); + do { + long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID); + String number = strequentCursor.getString(StrequentContactsCursorLoader.PHONE_NUMBER); + matchFound = contactId == entry.contactId || Objects.equals(number, entry.number); + } while (!matchFound && strequentCursor.moveToNext()); + return matchFound; + } + private static Cursor createHeaderCursor(@StringRes int header) { MatrixCursor cursor = new MatrixCursor(HEADER_CURSOR_PROJECTION); cursor.newRow().add(HEADER_CURSOR_PROJECTION[HEADER_COLUMN_POSITION], header); diff --git a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java index e9e3e32da..b8da47562 100644 --- a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java +++ b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java @@ -24,24 +24,27 @@ import android.net.Uri; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; +import com.android.dialer.speeddial.room.SpeedDialDatabaseComponent; +import com.android.dialer.speeddial.room.SpeedDialEntry; +import java.util.List; /** Cursor Loader for strequent contacts. */ -final class StrequentContactsCursorLoader extends CursorLoader { +public final class StrequentContactsCursorLoader extends CursorLoader { - static final int PHONE_ID = 0; - static final int PHONE_DISPLAY_NAME = 1; - static final int PHONE_STARRED = 2; - static final int PHONE_PHOTO_URI = 3; - static final int PHONE_LOOKUP_KEY = 4; - static final int PHONE_PHOTO_ID = 5; - static final int PHONE_NUMBER = 6; - static final int PHONE_TYPE = 7; - static final int PHONE_LABEL = 8; - static final int PHONE_IS_SUPER_PRIMARY = 9; - static final int PHONE_PINNED = 10; - static final int PHONE_CONTACT_ID = 11; + public static final int PHONE_ID = 0; + public static final int PHONE_DISPLAY_NAME = 1; + public static final int PHONE_STARRED = 2; + public static final int PHONE_PHOTO_URI = 3; + public static final int PHONE_LOOKUP_KEY = 4; + public static final int PHONE_PHOTO_ID = 5; + public static final int PHONE_NUMBER = 6; + public static final int PHONE_TYPE = 7; + public static final int PHONE_LABEL = 8; + public static final int PHONE_IS_SUPER_PRIMARY = 9; + public static final int PHONE_PINNED = 10; + public static final int PHONE_CONTACT_ID = 11; - static final String[] PHONE_PROJECTION = + public static final String[] PHONE_PROJECTION = new String[] { Phone._ID, // 0 Phone.DISPLAY_NAME, // 1 @@ -92,6 +95,12 @@ final class StrequentContactsCursorLoader extends CursorLoader { @Override public Cursor loadInBackground() { - return SpeedDialCursor.newInstance(super.loadInBackground()); + Cursor strequentCursor = super.loadInBackground(); + List entries = + SpeedDialDatabaseComponent.get(getContext().getApplicationContext()) + .speedDialDatabase() + .getSpeedDialEntryDao() + .getAllEntries(); + return SpeedDialCursor.newInstance(strequentCursor, entries); } } diff --git a/java/com/android/dialer/speeddial/room/SpeedDialDatabaseComponent.java b/java/com/android/dialer/speeddial/room/SpeedDialDatabaseComponent.java new file mode 100644 index 000000000..c415fd767 --- /dev/null +++ b/java/com/android/dialer/speeddial/room/SpeedDialDatabaseComponent.java @@ -0,0 +1,38 @@ +/* + * 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.speeddial.room; + +import android.content.Context; +import com.android.dialer.inject.HasRootComponent; +import dagger.Subcomponent; + +/** Dagger component for the speed dial room database. */ +@Subcomponent +public abstract class SpeedDialDatabaseComponent { + + public abstract SpeedDialEntryDatabase speedDialDatabase(); + + public static SpeedDialDatabaseComponent get(Context context) { + return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component()) + .speedDialDatabaseComponent(); + } + + /** Used to refer to the root application component. */ + public interface HasComponent { + SpeedDialDatabaseComponent speedDialDatabaseComponent(); + } +} diff --git a/java/com/android/dialer/speeddial/room/SpeedDialDatabaseModule.java b/java/com/android/dialer/speeddial/room/SpeedDialDatabaseModule.java new file mode 100644 index 000000000..8bb510087 --- /dev/null +++ b/java/com/android/dialer/speeddial/room/SpeedDialDatabaseModule.java @@ -0,0 +1,35 @@ +/* + * 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.speeddial.room; + +import android.content.Context; +import com.android.dialer.inject.ApplicationContext; +import dagger.Module; +import dagger.Provides; +import javax.inject.Singleton; + +/** Dagger module which satisfies speed dial database dependencies. */ +@Module +public class SpeedDialDatabaseModule { + + @Provides + @Singleton + static SpeedDialEntryDatabase provideSpeedDialEntryDatabase( + @ApplicationContext Context appContext) { + return SpeedDialEntryDatabase.create(appContext); + } +} diff --git a/java/com/android/dialer/speeddial/room/SpeedDialEntry.java b/java/com/android/dialer/speeddial/room/SpeedDialEntry.java new file mode 100644 index 000000000..37d2d8b98 --- /dev/null +++ b/java/com/android/dialer/speeddial/room/SpeedDialEntry.java @@ -0,0 +1,58 @@ +/* + * 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.speeddial.room; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** SpeedDialEntry Entity. Represents a single element in favorites. */ +@Entity +public class SpeedDialEntry { + + public static final int UNKNOWN = 0; + public static final int VOICE = 1; + public static final int VIDEO = 2; + + /** An Enum for the different row view types shown by this adapter. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({UNKNOWN, VOICE, VIDEO}) + public @interface Type {} + + @PrimaryKey(autoGenerate = true) + public Integer id; + + @Nullable public final String number; + + public final long contactId; + + @Type public final int type; + + /** Build an unknown speed dial entry. */ + public static SpeedDialEntry newSpeedDialEntry(long contactId) { + return new SpeedDialEntry(null, contactId, UNKNOWN); + } + + public SpeedDialEntry(@Nullable String number, long contactId, @Type int type) { + this.number = number; + this.contactId = contactId; + this.type = type; + } +} diff --git a/java/com/android/dialer/speeddial/room/SpeedDialEntryDao.java b/java/com/android/dialer/speeddial/room/SpeedDialEntryDao.java new file mode 100644 index 000000000..3e440deab --- /dev/null +++ b/java/com/android/dialer/speeddial/room/SpeedDialEntryDao.java @@ -0,0 +1,44 @@ +/* + * 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.speeddial.room; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Delete; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Update; +import java.util.List; + +/** Data access object for {@link SpeedDialEntry}. */ +@Dao +public interface SpeedDialEntryDao { + + @Query("SELECT * FROM speeddialentry") + List getAllEntries(); + + @Query("DELETE FROM speeddialentry") + void nukeTable(); + + @Insert + void insert(SpeedDialEntry... entries); + + @Update + void update(SpeedDialEntry... entries); + + @Delete + void delete(SpeedDialEntry... entries); +} diff --git a/java/com/android/dialer/speeddial/room/SpeedDialEntryDatabase.java b/java/com/android/dialer/speeddial/room/SpeedDialEntryDatabase.java new file mode 100644 index 000000000..ff8e23743 --- /dev/null +++ b/java/com/android/dialer/speeddial/room/SpeedDialEntryDatabase.java @@ -0,0 +1,55 @@ +/* + * 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.speeddial.room; + +import android.arch.persistence.room.Database; +import android.arch.persistence.room.Room; +import android.arch.persistence.room.RoomDatabase; +import android.content.Context; +import android.support.annotation.VisibleForTesting; +import javax.inject.Singleton; + +/** Database of {@link SpeedDialEntry}. */ +@Database( + entities = {SpeedDialEntry.class}, + // Version should not change unless SpeedDialEntry schema changes, then it should be incremented + version = 3 +) +@Singleton +public abstract class SpeedDialEntryDatabase extends RoomDatabase { + + private static final String DB_NAME = "speedDialEntryoDatabase.db"; + private static boolean allowMainThreadQueriesForTesting; + + /* package-private */ static SpeedDialEntryDatabase create(Context appContext) { + RoomDatabase.Builder builder = + Room.databaseBuilder(appContext, SpeedDialEntryDatabase.class, DB_NAME) + // TODO(calderwoodra): implement migration plan for database upgrades + .fallbackToDestructiveMigration(); + if (allowMainThreadQueriesForTesting) { + builder.allowMainThreadQueries(); + } + return builder.build(); + } + + public abstract SpeedDialEntryDao getSpeedDialEntryDao(); + + @VisibleForTesting + public static void allowMainThreadQueriesForTesting() { + allowMainThreadQueriesForTesting = true; + } +} -- cgit v1.2.3 From 07e323c0c61e2a9fc48bf91a02de6b19d2de1ece Mon Sep 17 00:00:00 2001 From: yueg Date: Tue, 19 Dec 2017 16:05:47 -0800 Subject: Bubble v2 RTL language fixes. - Always use LTR layout direction for root view to avoid jank animation. - Set menu button icon position (left or right) according to default locale. - Set bubble default showing position (left or right) according to default locale. Bug: 67605985 Test: NewBubbleIntegrationTest PiperOrigin-RevId: 179616379 Change-Id: If418cbbf4747c2b655bc83d7c06fc0139979d94b --- java/com/android/newbubble/NewBubble.java | 17 +++++++++++++++-- .../android/newbubble/res/layout/new_bubble_base.xml | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java index 3378ad81a..469c15d71 100644 --- a/java/com/android/newbubble/NewBubble.java +++ b/java/com/android/newbubble/NewBubble.java @@ -39,6 +39,7 @@ import android.support.annotation.VisibleForTesting; import android.support.v4.graphics.ColorUtils; import android.support.v4.os.BuildCompat; import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.text.TextUtils; import android.transition.TransitionManager; import android.transition.TransitionValues; import android.view.ContextThemeWrapper; @@ -70,6 +71,7 @@ import com.android.newbubble.NewBubbleInfo.Action; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; +import java.util.Locale; /** * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating, @@ -406,6 +408,8 @@ public class NewBubble { hideAfterText = false; + boolean isRtl = + TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; if (windowParams == null) { // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O. @SuppressWarnings("deprecation") @@ -423,7 +427,7 @@ public class NewBubble { | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_LAYOUT_NO_LIMITS, PixelFormat.TRANSLUCENT); - windowParams.gravity = Gravity.TOP | Gravity.LEFT; + windowParams.gravity = Gravity.TOP | (isRtl ? Gravity.RIGHT : Gravity.LEFT); windowParams.x = leftBoundary; windowParams.y = currentInfo.getStartingYPosition(); windowParams.height = LayoutParams.WRAP_CONTENT; @@ -441,6 +445,9 @@ public class NewBubble { viewHolder.getPrimaryButton().setScaleY(0); viewHolder.getPrimaryAvatar().setAlpha(0f); viewHolder.getPrimaryIcon().setAlpha(0f); + if (isRtl) { + onLeftRightSwitch(true); + } } viewHolder.setChildClickable(true); @@ -795,7 +802,13 @@ public class NewBubble { } private void configureButton(Action action, NewCheckableButton button) { - button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null); + boolean isRtl = + TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; + if (isRtl) { + button.setCompoundDrawablesWithIntrinsicBounds(null, null, action.getIconDrawable(), null); + } else { + button.setCompoundDrawablesWithIntrinsicBounds(action.getIconDrawable(), null, null, null); + } button.setChecked(action.isChecked()); button.setEnabled(action.isEnabled()); button.setText(action.getName()); diff --git a/java/com/android/newbubble/res/layout/new_bubble_base.xml b/java/com/android/newbubble/res/layout/new_bubble_base.xml index f83b75395..f6ce26dd1 100644 --- a/java/com/android/newbubble/res/layout/new_bubble_base.xml +++ b/java/com/android/newbubble/res/layout/new_bubble_base.xml @@ -21,6 +21,7 @@ android:layout_height="wrap_content" android:clipChildren="true" android:clipToPadding="false" + android:layoutDirection="ltr" tools:theme="@style/Theme.AppCompat"> Date: Tue, 19 Dec 2017 16:07:55 -0800 Subject: Remove voicemail from UI only after delete request. Bug: 64882313 Test: N/A PiperOrigin-RevId: 179616641 Change-Id: Ie9e67226dc7cd4082ca4f7fd3ca5725bb854bca2 --- java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java index 315bf1cf0..d94a21465 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java @@ -509,8 +509,6 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter Assert.checkArgument(expandedViewHolder.getViewHolderVoicemailUri().equals(voicemailUri)); - notifyItemRemoved(expandedViewHolder.getAdapterPosition()); - Assert.checkArgument(currentlyExpandedViewHolderId == expandedViewHolder.getViewHolderId()); collapseExpandedViewHolder(expandedViewHolder); @@ -524,6 +522,8 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter .onSuccess(deleteVoicemailCallBack) .build() .executeSerial(new Pair<>(context, voicemailUri)); + + notifyItemRemoved(expandedViewHolder.getAdapterPosition()); } private void onVoicemailDeleted(Integer integer) { -- cgit v1.2.3 From 2bf7d229b18ffdc6ebbc2f07ec3eaa685731e703 Mon Sep 17 00:00:00 2001 From: linyuh Date: Tue, 19 Dec 2017 16:39:31 -0800 Subject: Add SmartDialMaps for the Bulgarian alphabet and the Ukrainian alphabet. Bug: 30215380,70633239 Test: BulgarianSmartDialMapTest, UkrainianSmartDialMapTest PiperOrigin-RevId: 179621038 Change-Id: I1a5ad97ba7cd4e9e0edffb3cb39f40c4c5d137a1 --- .../dialer/smartdial/BulgarianSmartDialMap.java | 91 +++++++++++++++++++++ .../dialer/smartdial/CompositeSmartDialMap.java | 2 + .../dialer/smartdial/UkrainianSmartDialMap.java | 93 ++++++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 java/com/android/dialer/smartdial/BulgarianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/UkrainianSmartDialMap.java (limited to 'java/com/android') diff --git a/java/com/android/dialer/smartdial/BulgarianSmartDialMap.java b/java/com/android/dialer/smartdial/BulgarianSmartDialMap.java new file mode 100644 index 000000000..a16d15919 --- /dev/null +++ b/java/com/android/dialer/smartdial/BulgarianSmartDialMap.java @@ -0,0 +1,91 @@ +/* + * 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.smartdial; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Bulgarian alphabet. */ +@SuppressWarnings("Guava") +final class BulgarianSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + // Reference: https://en.wikipedia.org/wiki/Bulgarian_alphabet + static { + CHAR_TO_KEY_MAP.put('а', '2'); + CHAR_TO_KEY_MAP.put('б', '2'); + CHAR_TO_KEY_MAP.put('в', '2'); + CHAR_TO_KEY_MAP.put('г', '2'); + + CHAR_TO_KEY_MAP.put('д', '3'); + CHAR_TO_KEY_MAP.put('е', '3'); + CHAR_TO_KEY_MAP.put('ж', '3'); + CHAR_TO_KEY_MAP.put('з', '3'); + + CHAR_TO_KEY_MAP.put('и', '4'); + CHAR_TO_KEY_MAP.put('й', '4'); + CHAR_TO_KEY_MAP.put('к', '4'); + CHAR_TO_KEY_MAP.put('л', '4'); + + CHAR_TO_KEY_MAP.put('м', '5'); + CHAR_TO_KEY_MAP.put('н', '5'); + CHAR_TO_KEY_MAP.put('о', '5'); + + CHAR_TO_KEY_MAP.put('п', '6'); + CHAR_TO_KEY_MAP.put('р', '6'); + CHAR_TO_KEY_MAP.put('с', '6'); + + CHAR_TO_KEY_MAP.put('т', '7'); + CHAR_TO_KEY_MAP.put('у', '7'); + CHAR_TO_KEY_MAP.put('ф', '7'); + CHAR_TO_KEY_MAP.put('х', '7'); + + CHAR_TO_KEY_MAP.put('ц', '8'); + CHAR_TO_KEY_MAP.put('ч', '8'); + CHAR_TO_KEY_MAP.put('ш', '8'); + CHAR_TO_KEY_MAP.put('щ', '8'); + + CHAR_TO_KEY_MAP.put('ъ', '9'); + CHAR_TO_KEY_MAP.put('ь', '9'); + CHAR_TO_KEY_MAP.put('ю', '9'); + CHAR_TO_KEY_MAP.put('я', '9'); + } + + private static BulgarianSmartDialMap instance; + + static BulgarianSmartDialMap getInstance() { + if (instance == null) { + instance = new BulgarianSmartDialMap(); + } + + return instance; + } + + private BulgarianSmartDialMap() {} + + @Override + Optional normalizeCharacter(char ch) { + ch = Character.toLowerCase(ch); + return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} diff --git a/java/com/android/dialer/smartdial/CompositeSmartDialMap.java b/java/com/android/dialer/smartdial/CompositeSmartDialMap.java index d51e46f76..e9f237f01 100644 --- a/java/com/android/dialer/smartdial/CompositeSmartDialMap.java +++ b/java/com/android/dialer/smartdial/CompositeSmartDialMap.java @@ -45,7 +45,9 @@ public class CompositeSmartDialMap { private static final SimpleArrayMap EXTRA_MAPS = new SimpleArrayMap<>(); static { + EXTRA_MAPS.put("bul", BulgarianSmartDialMap.getInstance()); EXTRA_MAPS.put("rus", RussianSmartDialMap.getInstance()); + EXTRA_MAPS.put("ukr", UkrainianSmartDialMap.getInstance()); } private CompositeSmartDialMap() {} diff --git a/java/com/android/dialer/smartdial/UkrainianSmartDialMap.java b/java/com/android/dialer/smartdial/UkrainianSmartDialMap.java new file mode 100644 index 000000000..8ba53c45f --- /dev/null +++ b/java/com/android/dialer/smartdial/UkrainianSmartDialMap.java @@ -0,0 +1,93 @@ +/* + * 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.smartdial; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Ukrainian alphabet. */ +final class UkrainianSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + // Reference: https://en.wikipedia.org/wiki/Ukrainian_alphabet + static { + CHAR_TO_KEY_MAP.put('а', '2'); + CHAR_TO_KEY_MAP.put('б', '2'); + CHAR_TO_KEY_MAP.put('в', '2'); + CHAR_TO_KEY_MAP.put('г', '2'); + CHAR_TO_KEY_MAP.put('ґ', '2'); + + CHAR_TO_KEY_MAP.put('д', '3'); + CHAR_TO_KEY_MAP.put('е', '3'); + CHAR_TO_KEY_MAP.put('є', '3'); + CHAR_TO_KEY_MAP.put('ж', '3'); + CHAR_TO_KEY_MAP.put('з', '3'); + + CHAR_TO_KEY_MAP.put('и', '4'); + CHAR_TO_KEY_MAP.put('і', '4'); + CHAR_TO_KEY_MAP.put('ї', '4'); + CHAR_TO_KEY_MAP.put('й', '4'); + CHAR_TO_KEY_MAP.put('к', '4'); + CHAR_TO_KEY_MAP.put('л', '4'); + + CHAR_TO_KEY_MAP.put('м', '5'); + CHAR_TO_KEY_MAP.put('н', '5'); + CHAR_TO_KEY_MAP.put('о', '5'); + CHAR_TO_KEY_MAP.put('п', '5'); + + CHAR_TO_KEY_MAP.put('р', '6'); + CHAR_TO_KEY_MAP.put('с', '6'); + CHAR_TO_KEY_MAP.put('т', '6'); + CHAR_TO_KEY_MAP.put('у', '6'); + + CHAR_TO_KEY_MAP.put('ф', '7'); + CHAR_TO_KEY_MAP.put('х', '7'); + CHAR_TO_KEY_MAP.put('ц', '7'); + CHAR_TO_KEY_MAP.put('ч', '7'); + + CHAR_TO_KEY_MAP.put('ш', '8'); + CHAR_TO_KEY_MAP.put('щ', '8'); + + CHAR_TO_KEY_MAP.put('ь', '9'); + CHAR_TO_KEY_MAP.put('ю', '9'); + CHAR_TO_KEY_MAP.put('я', '9'); + } + + private static UkrainianSmartDialMap instance; + + static UkrainianSmartDialMap getInstance() { + if (instance == null) { + instance = new UkrainianSmartDialMap(); + } + + return instance; + } + + private UkrainianSmartDialMap() {} + + @Override + Optional normalizeCharacter(char ch) { + ch = Character.toLowerCase(ch); + return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} -- cgit v1.2.3 From a06002dcddc6bbf1d01350a2978d00f3c7665e6e Mon Sep 17 00:00:00 2001 From: uabdullah Date: Tue, 19 Dec 2017 17:02:50 -0800 Subject: Register content observer when voicemail table changes. When a new voicemail is received, it is written in the voicemail table by the Voicemail service. However the new voicemail will not get updated/shown in the New Voicemail UI as the annotated call log would be stale. This CL ensures that when the voicemail is added, the annotated call log is marked dirty and refreshed. This way the new voicemail will be shown in the new voicemail UI. Since a new voicemail is also added, we want to make sure the headers for "today" and "older", their positions are also updated accordingly. Bug: 64882313 Test: Unit tests PiperOrigin-RevId: 179623267 Change-Id: I5dfc84f62f9f37c57ffb2dbbe7e848a58306a19d --- .../systemcalllog/SystemCallLogDataSource.java | 25 ++++++++++++++++------ .../voicemail/listui/NewVoicemailAdapter.java | 19 ++++++++++++++-- 2 files changed, 36 insertions(+), 8 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index 0ed185966..95fbf9d04 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -27,6 +27,7 @@ import android.os.Build; import android.os.Handler; import android.provider.CallLog; import android.provider.CallLog.Calls; +import android.provider.VoicemailContract; import android.support.annotation.ColorInt; import android.support.annotation.MainThread; import android.support.annotation.Nullable; @@ -92,13 +93,21 @@ public class SystemCallLogDataSource implements CallLogDataSource { } // TODO(zachh): Need to somehow register observers if user enables permission after launch? + CallLogObserver callLogObserver = + new CallLogObserver(ThreadUtil.getUiThreadHandler(), appContext, contentObserverCallbacks); + appContext .getContentResolver() - .registerContentObserver( - CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, - true, - new CallLogObserver( - ThreadUtil.getUiThreadHandler(), appContext, contentObserverCallbacks)); + .registerContentObserver(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, true, callLogObserver); + + if (!PermissionsUtil.hasAddVoicemailPermissions(appContext)) { + LogUtil.i("SystemCallLogDataSource.registerContentObservers", "no add voicemail permissions"); + return; + } + // TODO(uabdullah): Need to somehow register observers if user enables permission after launch? + appContext + .getContentResolver() + .registerContentObserver(VoicemailContract.Status.CONTENT_URI, true, callLogObserver); } @Override @@ -462,7 +471,11 @@ public class SystemCallLogDataSource implements CallLogDataSource { @Override public void onChange(boolean selfChange, Uri uri) { Assert.isMainThread(); - LogUtil.enterBlock("SystemCallLogDataSource.CallLogObserver.onChange"); + LogUtil.i( + "SystemCallLogDataSource.CallLogObserver.onChange", + "Uri:%s, SelfChange:%b", + String.valueOf(uri), + selfChange); super.onChange(selfChange, uri); /* diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java index d94a21465..93d5cda3e 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java @@ -67,9 +67,9 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter private final Clock clock; /** {@link Integer#MAX_VALUE} when the "Today" header should not be displayed. */ - private final int todayHeaderPosition; + private int todayHeaderPosition = Integer.MAX_VALUE; /** {@link Integer#MAX_VALUE} when the "Older" header should not be displayed. */ - private final int olderHeaderPosition; + private int olderHeaderPosition = Integer.MAX_VALUE; private final FragmentManager fragmentManager; /** A valid id for {@link VoicemailEntry} is greater than 0 */ @@ -107,7 +107,15 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter this.clock = clock; this.fragmentManager = fragmentManager; initializeMediaPlayerListeners(); + updateHeaderPositions(); + } + private void updateHeaderPositions() { + LogUtil.i( + "NewVoicemailAdapter.updateHeaderPositions", + "before updating todayPos:%d, olderPos:%d", + todayHeaderPosition, + olderHeaderPosition); // Calculate header adapter positions by reading cursor. long currentTimeMillis = clock.currentTimeMillis(); if (cursor.moveToNext()) { @@ -134,6 +142,11 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter this.todayHeaderPosition = Integer.MAX_VALUE; this.olderHeaderPosition = Integer.MAX_VALUE; } + LogUtil.i( + "NewVoicemailAdapter.updateHeaderPositions", + "after updating todayPos:%d, olderPos:%d", + todayHeaderPosition, + olderHeaderPosition); } private void initializeMediaPlayerListeners() { @@ -143,8 +156,10 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter } public void updateCursor(Cursor updatedCursor) { + LogUtil.enterBlock("NewVoicemailAdapter.updateCursor"); deletedVoicemailPosition.clear(); this.cursor = updatedCursor; + updateHeaderPositions(); notifyDataSetChanged(); } -- cgit v1.2.3 From 0ca01853cb89666578261f31b58ce80061029a56 Mon Sep 17 00:00:00 2001 From: maxwelb Date: Tue, 19 Dec 2017 17:47:12 -0800 Subject: Don't use phone number in spam call notifications Bug: 67775111 Test: Tap for regressions, manually verified bugreport didn't include PII PiperOrigin-RevId: 179628074 Change-Id: If9ee7fcfe709e749da33d0bddf2f7847e68cc422 --- java/com/android/incallui/spam/SpamCallListListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'java/com/android') diff --git a/java/com/android/incallui/spam/SpamCallListListener.java b/java/com/android/incallui/spam/SpamCallListListener.java index fa3dd6e01..e7603f041 100644 --- a/java/com/android/incallui/spam/SpamCallListListener.java +++ b/java/com/android/incallui/spam/SpamCallListListener.java @@ -455,6 +455,6 @@ public class SpamCallListListener implements CallList.Listener { } static String getNotificationTagForCall(@NonNull DialerCall call) { - return NOTIFICATION_TAG_PREFIX + call.getNumber(); + return NOTIFICATION_TAG_PREFIX + call.getUniqueCallId(); } } -- cgit v1.2.3 From b05a2b45c37ebc2f4f9fcb36907177c5211622d4 Mon Sep 17 00:00:00 2001 From: zachh Date: Tue, 19 Dec 2017 17:54:37 -0800 Subject: Include inserted calls for consideration in PhoneLookupDataSource. If there is a new call with a number that hasn't been seen before, it should be considered the same as numbers that are already part of the call log. Bug: 34672501 Test: unit PiperOrigin-RevId: 179628789 Change-Id: I422c24c444958dd8842aa14cf8a8069da5cec2c1 --- .../phonelookup/PhoneLookupDataSource.java | 36 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index fbb58312a..042ff30a2 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -110,7 +110,9 @@ public final class PhoneLookupDataSource implements CallLogDataSource { *

This method uses the following algorithm: * *

    - *
  • Selects the distinct DialerPhoneNumbers from the AnnotatedCallLog + *
  • Finds the phone numbers of interest by taking the union of the distinct + * DialerPhoneNumbers from the AnnotatedCallLog and the pending inserts provided in {@code + * mutations} *
  • Uses them to fetch the current information from PhoneLookupHistory, in order to construct * a map from DialerPhoneNumber to PhoneLookupInfo *
      @@ -137,9 +139,10 @@ public final class PhoneLookupDataSource implements CallLogDataSource { phoneLookupHistoryRowsToUpdate.clear(); phoneLookupHistoryRowsToDelete.clear(); - // First query information from annotated call log. + // First query information from annotated call log (and include pending inserts). ListenableFuture>> annotatedCallLogIdsByNumberFuture = - backgroundExecutorService.submit(() -> queryIdAndNumberFromAnnotatedCallLog(appContext)); + backgroundExecutorService.submit( + () -> collectIdAndNumberFromAnnotatedCallLogAndPendingInserts(appContext, mutations)); // Use it to create the original info map. ListenableFuture> originalInfoMapFuture = @@ -317,9 +320,28 @@ public final class PhoneLookupDataSource implements CallLogDataSource { return numbers.build(); } - private Map> queryIdAndNumberFromAnnotatedCallLog( - Context appContext) { + private Map> collectIdAndNumberFromAnnotatedCallLogAndPendingInserts( + Context appContext, CallLogMutations mutations) { Map> idsByNumber = new ArrayMap<>(); + // First add any pending inserts to the map. + for (Entry entry : mutations.getInserts().entrySet()) { + long id = entry.getKey(); + ContentValues insertedContentValues = entry.getValue(); + DialerPhoneNumber dialerPhoneNumber; + try { + dialerPhoneNumber = + DialerPhoneNumber.parseFrom( + insertedContentValues.getAsByteArray(AnnotatedCallLog.NUMBER)); + } catch (InvalidProtocolBufferException e) { + throw new IllegalStateException(e); + } + Set ids = idsByNumber.get(dialerPhoneNumber); + if (ids == null) { + ids = new ArraySet<>(); + idsByNumber.put(dialerPhoneNumber, ids); + } + ids.add(id); + } try (Cursor cursor = appContext @@ -332,7 +354,9 @@ public final class PhoneLookupDataSource implements CallLogDataSource { null)) { if (cursor == null) { - LogUtil.e("PhoneLookupDataSource.queryIdAndNumberFromAnnotatedCallLog", "null cursor"); + LogUtil.e( + "PhoneLookupDataSource.collectIdAndNumberFromAnnotatedCallLogAndPendingInserts", + "null cursor"); return ImmutableMap.of(); } -- cgit v1.2.3 From 043d6715d7b7f5ccc81c416ef80c1fc04ec1445a Mon Sep 17 00:00:00 2001 From: roldenburg Date: Tue, 19 Dec 2017 18:05:16 -0800 Subject: Always have hardware acceration enabled for InCallActivity This is necessary because Go variants have hardware acceleration disabled at the application level for memory usage reasons. Author: srafeeqh@qti.qualcomm.com Bug: 69490087,70200603 Test: test video call PiperOrigin-RevId: 179629786 Change-Id: Idc900200f41bd1ae7920310b900df5b6e64479b9 --- java/com/android/incallui/AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) (limited to 'java/com/android') diff --git a/java/com/android/incallui/AndroidManifest.xml b/java/com/android/incallui/AndroidManifest.xml index b9d481b31..a98cc91d2 100644 --- a/java/com/android/incallui/AndroidManifest.xml +++ b/java/com/android/incallui/AndroidManifest.xml @@ -47,10 +47,13 @@ android:name="android.telephony.hide_voicemail_settings_menu" android:value="true"/> + Date: Tue, 19 Dec 2017 19:43:53 -0800 Subject: Bubble v2 changes. - Only show bubble for outgoing, active and background call. (Before: show bubble when is in call) - Show "Call ended" and hide bubble only when there is no outgoing, active or background call. (Before: show "Call ended" for all real call disconnection like one call in conference) - Don't show "Call ended" but only hide bubble when Duo upgrade is accepted/declined. We show bubble for Duo upgrade since the call is still connected. The solution doesn't work for fallback upgrade on pre-ODR device (so "Call ended" still shows). Bug: 67605985 Test: NewReturnToCallControllerTest PiperOrigin-RevId: 179636643 Change-Id: I5d1f6e812c94108228757af89e33d4c496beb735 --- java/com/android/dialer/telecom/TelecomUtil.java | 10 ++++- .../incallui/NewReturnToCallController.java | 43 ++++++++++------------ 2 files changed, 28 insertions(+), 25 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java index 22f3727e6..c64a50231 100644 --- a/java/com/android/dialer/telecom/TelecomUtil.java +++ b/java/com/android/dialer/telecom/TelecomUtil.java @@ -178,6 +178,10 @@ public abstract class TelecomUtil { * are not included. */ public static boolean isInManagedCall(Context context) { + return instance.isInManagedCall(context); + } + + public static boolean isInCall(Context context) { return instance.isInCall(context); } @@ -289,7 +293,7 @@ public abstract class TelecomUtil { @VisibleForTesting() public static class TelecomUtilImpl { - public boolean isInCall(Context context) { + public boolean isInManagedCall(Context context) { if (hasReadPhoneStatePermission(context)) { // The TelecomManager#isInCall method returns true anytime the user is in a call. // Starting in O, the APIs include support for self-managed ConnectionServices so that other @@ -308,6 +312,10 @@ public abstract class TelecomUtil { return false; } + public boolean isInCall(Context context) { + return hasReadPhoneStatePermission(context) && getTelecomManager(context).isInCall(); + } + public boolean hasPermission(Context context, String permission) { return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; diff --git a/java/com/android/incallui/NewReturnToCallController.java b/java/com/android/incallui/NewReturnToCallController.java index ad49d6828..ca60a52c1 100644 --- a/java/com/android/incallui/NewReturnToCallController.java +++ b/java/com/android/incallui/NewReturnToCallController.java @@ -101,7 +101,7 @@ public class NewReturnToCallController implements InCallUiListener, Listener, Au if (showing) { hide(); } else { - if (TelecomUtil.isInManagedCall(context)) { + if (getCall() != null) { show(); } } @@ -157,22 +157,15 @@ public class NewReturnToCallController implements InCallUiListener, Listener, Au @Override public void onDisconnect(DialerCall call) { - if (call.wasParentCall()) { - // It's disconnected after the last child call is disconnected, and we already did everything - // for the last child. - LogUtil.i( - "ReturnToCallController.onDisconnect", "being called for a parent call and do nothing"); - return; - } - if (bubble != null - && bubble.isVisible() - && (!TelecomUtil.isInManagedCall(context) - || CallList.getInstance().getActiveOrBackgroundCall() != null)) { - bubble.showText(context.getText(R.string.incall_call_ended)); - } - // For conference call, we should hideAndReset for the last disconnected child call while the - // parent call is still there. - if (!CallList.getInstance().hasNonParentActiveOrBackgroundCall()) { + LogUtil.enterBlock("ReturnToCallController.onDisconnect"); + if (bubble != null && bubble.isVisible() && (getCall() == null)) { + // Show "Call ended" and hide bubble when there is no outgoing, active or background call + LogUtil.i("ReturnToCallController.onDisconnect", "show call ended and hide bubble"); + // Don't show text if it's Duo upgrade + // It doesn't work for Duo fallback upgrade since we're not considered in call + if (!TelecomUtil.isInCall(context) || CallList.getInstance().getIncomingCall() != null) { + bubble.showText(context.getText(R.string.incall_call_ended)); + } hideAndReset(); } else { startContactInfoSearch(); @@ -197,19 +190,21 @@ public class NewReturnToCallController implements InCallUiListener, Listener, Au } private void startContactInfoSearch() { - DialerCall dialerCall = CallList.getInstance().getIncomingCall(); - if (dialerCall == null) { - dialerCall = CallList.getInstance().getOutgoingCall(); - } - if (dialerCall == null) { - dialerCall = CallList.getInstance().getActiveOrBackgroundCall(); - } + DialerCall dialerCall = getCall(); if (dialerCall != null) { contactInfoCache.findInfo( dialerCall, false /* isIncoming */, new ReturnToCallContactInfoCacheCallback(this)); } } + private DialerCall getCall() { + DialerCall dialerCall = CallList.getInstance().getOutgoingCall(); + if (dialerCall == null) { + dialerCall = CallList.getInstance().getActiveOrBackgroundCall(); + } + return dialerCall; + } + private void onPhotoAvatarReceived(@NonNull Drawable photo) { if (bubble != null) { bubble.updatePhotoAvatar(photo); -- cgit v1.2.3 From 0fab10eb28ab1e246f572302ad0e12508a196b34 Mon Sep 17 00:00:00 2001 From: zachh Date: Wed, 20 Dec 2017 10:57:38 -0800 Subject: Added copySubMessage method to PhoneLookup interface. The existing way that protos are merged in CompositePhoneLookup is not correct because foo_submessage from BarDataSource may incorrectly contribute old information to the merged message. The new copySubMessage method makes it so that each PhoneLookup is responsible for defining which submessage it is responsible for and prevents the problem. Test: unit PiperOrigin-RevId: 179707015 Change-Id: I566305cf64c46c698f14812d9241d166ac75a6d3 --- java/com/android/dialer/function/BiConsumer.java | 24 ++++++++++++++++++++++ .../android/dialer/phonelookup/PhoneLookup.java | 6 ++++++ .../composite/CompositePhoneLookup.java | 10 +++++++-- .../dialer/phonelookup/cp2/Cp2PhoneLookup.java | 6 ++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 java/com/android/dialer/function/BiConsumer.java (limited to 'java/com/android') diff --git a/java/com/android/dialer/function/BiConsumer.java b/java/com/android/dialer/function/BiConsumer.java new file mode 100644 index 000000000..50052210e --- /dev/null +++ b/java/com/android/dialer/function/BiConsumer.java @@ -0,0 +1,24 @@ +/* + * 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.function; + +/** Functional interface for consuming two generic values. */ +public interface BiConsumer { + + /** Consumes a value. */ + void accept(T t, U u); +} diff --git a/java/com/android/dialer/phonelookup/PhoneLookup.java b/java/com/android/dialer/phonelookup/PhoneLookup.java index eeab4dadd..bb14c1ff6 100644 --- a/java/com/android/dialer/phonelookup/PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/PhoneLookup.java @@ -61,6 +61,12 @@ public interface PhoneLookup { ListenableFuture> getMostRecentPhoneLookupInfo( ImmutableMap existingInfoMap); + /** + * Populates the sub-message that this {@link PhoneLookup} is responsible for by copying the + * sub-message value from {@code source} to {@code destination} + */ + void copySubMessage(PhoneLookupInfo.Builder destination, PhoneLookupInfo source); + /** * Called when the results of the {@link #getMostRecentPhoneLookupInfo(ImmutableMap)} have been * applied by the caller. diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java index ee2244615..bb7856fff 100644 --- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java +++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java @@ -112,14 +112,15 @@ public final class CompositePhoneLookup implements PhoneLookup { ImmutableMap.builder(); for (DialerPhoneNumber dialerPhoneNumber : existingInfoMap.keySet()) { PhoneLookupInfo.Builder combinedInfo = PhoneLookupInfo.newBuilder(); - for (ImmutableMap map : allMaps) { + for (int i = 0; i < allMaps.size(); i++) { + ImmutableMap map = allMaps.get(i); 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); + phoneLookups.get(i).copySubMessage(combinedInfo, subInfo); } combinedMap.put(dialerPhoneNumber, combinedInfo.build()); } @@ -128,6 +129,11 @@ public final class CompositePhoneLookup implements PhoneLookup { lightweightExecutorService); } + @Override + public void copySubMessage(PhoneLookupInfo.Builder destination, PhoneLookupInfo source) { + throw new UnsupportedOperationException(); + } + @Override public ListenableFuture onSuccessfulBulkUpdate() { List> futures = new ArrayList<>(); diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java index b31d0e72e..4261b763a 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java @@ -131,6 +131,7 @@ public final class Cp2PhoneLookup implements PhoneLookup { private boolean isDirtyInternal(ImmutableSet phoneNumbers) { long lastModified = sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L); + // TODO(zachh): If a number got disassociated with a contact; the contactIds will be empty. return contactsUpdated(queryPhoneTableForContactIds(phoneNumbers), lastModified) || contactsDeleted(lastModified); } @@ -253,6 +254,11 @@ public final class Cp2PhoneLookup implements PhoneLookup { () -> getMostRecentPhoneLookupInfoInternal(existingInfoMap)); } + @Override + public void copySubMessage(PhoneLookupInfo.Builder destination, PhoneLookupInfo source) { + destination.setCp2Info(source.getCp2Info()); + } + private ImmutableMap getMostRecentPhoneLookupInfoInternal( ImmutableMap existingInfoMap) { currentLastTimestampProcessed = null; -- cgit v1.2.3 From 705c152bc910125064459c5808334d8cad00536c Mon Sep 17 00:00:00 2001 From: Android Dialer Date: Wed, 20 Dec 2017 11:27:33 -0800 Subject: Bug: 70402588 Test: Bubble.expand PiperOrigin-RevId: 179711507 Change-Id: I4b83d46fe64ea87a1d7cbe3af863b49884dfcad4 --- java/com/android/bubble/res/values-car/values.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/bubble/res/values-car/values.xml b/java/com/android/bubble/res/values-car/values.xml index cf8839a84..47f20a1bf 100644 --- a/java/com/android/bubble/res/values-car/values.xml +++ b/java/com/android/bubble/res/values-car/values.xml @@ -18,10 +18,8 @@ 64dp - 54dp + 50dp 38dp - 10dp - 18dp - 18dp + 12dp -- cgit v1.2.3 From 7c711fa89ab258e946b2440d94215de8ec18cb36 Mon Sep 17 00:00:00 2001 From: zachh Date: Wed, 20 Dec 2017 13:32:01 -0800 Subject: Fixed crash in PinnedShortcuts. It was expecting only shortcuts for contacts but we also have the "NUI Launcher" shortcut now, so ignore it when attempting to update. Test: unit PiperOrigin-RevId: 179726507 Change-Id: I45e1594250ed70fcd1d18bfead5a9b4e9e46a58b --- java/com/android/dialer/shortcuts/PinnedShortcuts.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/shortcuts/PinnedShortcuts.java b/java/com/android/dialer/shortcuts/PinnedShortcuts.java index bfcc3df81..6e23a5c21 100644 --- a/java/com/android/dialer/shortcuts/PinnedShortcuts.java +++ b/java/com/android/dialer/shortcuts/PinnedShortcuts.java @@ -46,7 +46,6 @@ import java.util.Map; *

      When refreshing pinned shortcuts, we check to make sure that pinned contact information is * still up to date (e.g. photo and name). We also check to see if the contact has been deleted from * the user's contacts, and if so, we disable the pinned shortcut. - * */ @TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1 final class PinnedShortcuts { @@ -107,6 +106,14 @@ final class PinnedShortcuts { // setRank is nonsensical for pinned shortcuts and therefore could not be calculated. continue; } + // Exclude shortcuts like the "Phone NUI" shortcut. + String action = null; + if (shortcutInfo.getIntent() != null) { + action = shortcutInfo.getIntent().getAction(); + } + if (action == null || !action.equals("com.android.dialer.shortcuts.CALL_CONTACT")) { + continue; + } String lookupKey = DialerShortcut.getLookupKeyFromShortcutInfo(shortcutInfo); Uri lookupUri = DialerShortcut.getLookupUriFromShortcutInfo(shortcutInfo); -- cgit v1.2.3 From 6456d8302885e9c6f24d678fca798b730d101a17 Mon Sep 17 00:00:00 2001 From: zachh Date: Wed, 20 Dec 2017 13:35:19 -0800 Subject: Handle contacts which have been disassociated with a number in Cp2PhoneLookup. Bug: 34672501 Test: unit PiperOrigin-RevId: 179726904 Change-Id: I6a81ef28675af7f95139193b69f87decddc4c844 --- .../dialer/phonelookup/cp2/Cp2PhoneLookup.java | 103 +++++++++++++++++++-- 1 file changed, 97 insertions(+), 6 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java index 4261b763a..a477e035c 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java @@ -36,6 +36,7 @@ import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info; import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info.Cp2ContactInfo; +import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.android.dialer.storage.Unencrypted; import com.android.dialer.telecom.TelecomCallUtil; @@ -45,6 +46,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.protobuf.InvalidProtocolBufferException; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -131,9 +133,25 @@ public final class Cp2PhoneLookup implements PhoneLookup { private boolean isDirtyInternal(ImmutableSet phoneNumbers) { long lastModified = sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L); - // TODO(zachh): If a number got disassociated with a contact; the contactIds will be empty. - return contactsUpdated(queryPhoneTableForContactIds(phoneNumbers), lastModified) - || contactsDeleted(lastModified); + // We are always going to need to do this check and it is pretty cheap so do it first. + if (anyContactsDeletedSince(lastModified)) { + return true; + } + // Hopefully the most common case is there are no contacts updated; we can detect this cheaply. + if (noContactsModifiedSince(lastModified)) { + return false; + } + // This method is more expensive but is probably the most likely scenario; we are looking for + // changes to contacts which have been called. + if (contactsUpdated(queryPhoneTableForContactIds(phoneNumbers), lastModified)) { + return true; + } + // This is the most expensive method so do it last; the scenario is that a contact which has + // been called got disassociated with a number and we need to clear their information. + if (contactsUpdated(queryPhoneLookupHistoryForContactIds(), lastModified)) { + return true; + } + return false; } /** @@ -156,6 +174,46 @@ public final class Cp2PhoneLookup implements PhoneLookup { return contactIds; } + /** Gets all of the contact ids from PhoneLookupHistory. */ + private Set queryPhoneLookupHistoryForContactIds() { + Set contactIds = new ArraySet<>(); + try (Cursor cursor = + appContext + .getContentResolver() + .query( + PhoneLookupHistory.CONTENT_URI, + new String[] { + PhoneLookupHistory.PHONE_LOOKUP_INFO, + }, + null, + null, + null)) { + + if (cursor == null) { + LogUtil.w("Cp2PhoneLookup.queryPhoneLookupHistoryForContactIds", "null cursor"); + return contactIds; + } + + if (cursor.moveToFirst()) { + int phoneLookupInfoColumn = + cursor.getColumnIndexOrThrow(PhoneLookupHistory.PHONE_LOOKUP_INFO); + do { + PhoneLookupInfo phoneLookupInfo; + try { + phoneLookupInfo = PhoneLookupInfo.parseFrom(cursor.getBlob(phoneLookupInfoColumn)); + } catch (InvalidProtocolBufferException e) { + throw new IllegalStateException(e); + } + for (Cp2ContactInfo info : phoneLookupInfo.getCp2Info().getCp2ContactInfoList()) { + contactIds.add(info.getContactId()); + } + } while (cursor.moveToNext()); + } + } + + return contactIds; + } + private Set queryPhoneTableForContactIdsBasedOnE164(Set validE164Numbers) { Set contactIds = new ArraySet<>(); if (validE164Numbers.isEmpty()) { @@ -227,8 +285,26 @@ public final class Cp2PhoneLookup implements PhoneLookup { null); } + private boolean noContactsModifiedSince(long lastModified) { + try (Cursor cursor = + appContext + .getContentResolver() + .query( + Contacts.CONTENT_URI, + new String[] {Contacts._ID}, + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?", + new String[] {Long.toString(lastModified)}, + Contacts._ID + " limit 1")) { + if (cursor == null) { + LogUtil.w("Cp2PhoneLookup.noContactsModifiedSince", "null cursor"); + return false; + } + return cursor.getCount() == 0; + } + } + /** Returns true if any contacts were deleted after {@code lastModified}. */ - private boolean contactsDeleted(long lastModified) { + private boolean anyContactsDeletedSince(long lastModified) { try (Cursor cursor = appContext .getContentResolver() @@ -237,9 +313,9 @@ public final class Cp2PhoneLookup implements PhoneLookup { new String[] {DeletedContacts.CONTACT_DELETED_TIMESTAMP}, DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?", new String[] {Long.toString(lastModified)}, - null)) { + DeletedContacts.CONTACT_DELETED_TIMESTAMP + " limit 1")) { if (cursor == null) { - LogUtil.w("Cp2PhoneLookup.contactsDeleted", "null cursor"); + LogUtil.w("Cp2PhoneLookup.anyContactsDeletedSince", "null cursor"); return false; } return cursor.getCount() > 0; @@ -413,6 +489,11 @@ public final class Cp2PhoneLookup implements PhoneLookup { partitionedNumbers.dialerPhoneNumbersForE164(e164Number); Cp2ContactInfo info = buildCp2ContactInfoFromPhoneCursor(appContext, cursor); addInfo(map, dialerPhoneNumbers, info); + + // We are going to remove the numbers that we've handled so that we later can detect + // numbers that weren't handled and therefore need to have their contact information + // removed. + updatedNumbers.removeAll(dialerPhoneNumbers); } } } @@ -430,10 +511,20 @@ public final class Cp2PhoneLookup implements PhoneLookup { partitionedNumbers.dialerPhoneNumbersForUnformattable(unformattableNumber); Cp2ContactInfo info = buildCp2ContactInfoFromPhoneCursor(appContext, cursor); addInfo(map, dialerPhoneNumbers, info); + + // We are going to remove the numbers that we've handled so that we later can detect + // numbers that weren't handled and therefore need to have their contact information + // removed. + updatedNumbers.removeAll(dialerPhoneNumbers); } } } } + // The leftovers in updatedNumbers that weren't removed are numbers that were previously + // associated with contacts, but are no longer. Remove the contact information for them. + for (DialerPhoneNumber dialerPhoneNumber : updatedNumbers) { + map.put(dialerPhoneNumber, ImmutableSet.of()); + } return map; } -- cgit v1.2.3 From 128d859aa0e678971c85d00d4417c9ca79a8b702 Mon Sep 17 00:00:00 2001 From: zachh Date: Wed, 20 Dec 2017 14:32:47 -0800 Subject: Fixed crash in UiListener when launching activity with screen off. When launching MainActivity with the screen off (e.g. from Android Studio) the application would crash due to: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState Also use FragmentPagerAdapter in MainPagerAdapter as the number of tabs is small and can be stored in memory. Test: manual PiperOrigin-RevId: 179734952 Change-Id: Ib2ca9674f3174493da55bbbf0ef4053fcf73ab47 --- java/com/android/dialer/calllog/ui/NewCallLogFragment.java | 6 +++--- java/com/android/dialer/common/concurrent/UiListener.java | 8 +++++++- java/com/android/dialer/main/impl/MainPagerAdapter.java | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java index c10b52123..719878cec 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java @@ -58,10 +58,10 @@ public final class NewCallLogFragment extends Fragment } @Override - public void onCreate(Bundle state) { - super.onCreate(state); + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); - LogUtil.enterBlock("NewCallLogFragment.onCreate"); + LogUtil.enterBlock("NewCallLogFragment.onActivityCreated"); CallLogComponent component = CallLogComponent.get(getContext()); CallLogFramework callLogFramework = component.callLogFramework(); diff --git a/java/com/android/dialer/common/concurrent/UiListener.java b/java/com/android/dialer/common/concurrent/UiListener.java index 9541dbc0c..df791301f 100644 --- a/java/com/android/dialer/common/concurrent/UiListener.java +++ b/java/com/android/dialer/common/concurrent/UiListener.java @@ -71,7 +71,10 @@ public class UiListener extends Fragment { if (uiListener == null) { LogUtil.i("UiListener.create", "creating new UiListener for " + taskId); uiListener = new UiListener<>(); - fragmentManager.beginTransaction().add(uiListener, taskId).commit(); + // When launching an activity with the screen off, its onSaveInstanceState() is called before + // its fragments are created, which means we can't use commit() and need to use + // commitAllowingStateLoss(). This is not a problem for UiListener which saves no state. + fragmentManager.beginTransaction().add(uiListener, taskId).commitAllowingStateLoss(); } return uiListener; } @@ -130,6 +133,9 @@ public class UiListener extends Fragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); + // Note: We use commitAllowingStateLoss when attaching the fragment so it may not be safe to + // read savedInstanceState in all situations. (But it's not anticipated that this fragment + // should need to rely on saved state.) } @Override diff --git a/java/com/android/dialer/main/impl/MainPagerAdapter.java b/java/com/android/dialer/main/impl/MainPagerAdapter.java index 2d224f69c..d294640ee 100644 --- a/java/com/android/dialer/main/impl/MainPagerAdapter.java +++ b/java/com/android/dialer/main/impl/MainPagerAdapter.java @@ -20,7 +20,7 @@ import android.content.Context; import android.support.annotation.IntDef; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.app.FragmentPagerAdapter; import com.android.dialer.calllog.ui.NewCallLogFragment; import com.android.dialer.common.Assert; import com.android.dialer.voicemail.listui.NewVoicemailFragment; @@ -28,7 +28,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** Adapter for {@link MainActivity} ViewPager. */ -final class MainPagerAdapter extends FragmentStatePagerAdapter { +final class MainPagerAdapter extends FragmentPagerAdapter { @Retention(RetentionPolicy.SOURCE) @IntDef({ -- cgit v1.2.3 From 9709446856e01c23cfd02f83f85ecd79bdd8f785 Mon Sep 17 00:00:00 2001 From: weijiaxu Date: Wed, 20 Dec 2017 18:44:55 -0800 Subject: Quick fix for bugs on simulator voice call. -Fix incoming spam call which doesn't respond to UI interaction. Reason for that was connectionTag didn't get assigned. -Fix dialog of custom incoming/outgoing call. Current value of callerIdPresentationChoice is 0 which leads dialer to show UNKNOWN number on InCallUiActivity. Set callerIdPresentationChoice to 1 (ALLOWED) as default value. Test: on a local device. PiperOrigin-RevId: 179760724 Change-Id: I68f6e238ecd78a017d6539b25f95645c72e2cd0f --- java/com/android/dialer/simulator/impl/SimulatorDialogFragment.java | 3 ++- java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/simulator/impl/SimulatorDialogFragment.java b/java/com/android/dialer/simulator/impl/SimulatorDialogFragment.java index f8403c7fe..96ea62739 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorDialogFragment.java +++ b/java/com/android/dialer/simulator/impl/SimulatorDialogFragment.java @@ -29,7 +29,7 @@ public final class SimulatorDialogFragment extends DialogFragment { private final String[] callerIdPresentationItems = { "ALLOWED", "PAYPHONE", "RESTRICTED", "UNKNOWN" }; - private int callerIdPresentationChoice; + private int callerIdPresentationChoice = 1; private DialogCallback dialogCallback; @@ -47,6 +47,7 @@ public final class SimulatorDialogFragment extends DialogFragment { public Dialog onCreateDialog(Bundle bundle) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); final EditText input = new EditText(getActivity()); + input.setHint("Please input phone number"); builder .setTitle("Phone Number:") .setView(input) diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java index 89c5d2f14..ff00dd87e 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java +++ b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java @@ -120,7 +120,8 @@ final class SimulatorVoiceCall private void addSpamIncomingCall() { String callerId = "+1-661-778-3020"; /* Blacklisted custom spam number */ - SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */); + connectionTag = + SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */); } private void addNewEmergencyCallBack() { -- cgit v1.2.3 From 2cf2c3484c5f8dd11e6ad32633f7254119525413 Mon Sep 17 00:00:00 2001 From: erfanian Date: Thu, 21 Dec 2017 12:01:33 -0800 Subject: Update assisted dialing extras in preparation for platform implementation. * Use only one extra as if we were going to make a request of the Platform. * Modify the incallui to handle instances where the platform may not supply TransformationInfo. This should accommodate instances where the platform "used" assisted dialing, but did not provide the necessary TransformationInfo. Test: unit tests PiperOrigin-RevId: 179841752 Change-Id: I06411dc00812dba8978a2a090d8769dcce9b2ad6 --- .../compat/telephony/TelephonyManagerCompat.java | 14 ++----- .../dialer/precall/impl/AssistedDialAction.java | 6 ++- java/com/android/incallui/call/DialerCall.java | 43 +++++++++++++++++++--- java/com/android/incallui/contactgrid/TopRow.java | 2 +- 4 files changed, 47 insertions(+), 18 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java index 6bed818da..b01689da4 100644 --- a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java +++ b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java @@ -57,22 +57,16 @@ public class TelephonyManagerCompat { * *

      This signals to the telephony platform that an outgoing call qualifies for assisted dialing. */ - public static final String ALLOW_ASSISTED_DIAL = "android.telecom.extra.ALLOW_ASSISTED_DIAL"; - - // TODO(erfanian): a bug Replace with the platform/telecom constant when available. - /** - * Indicates that an outgoing call has undergone assisted dialing. - * - *

      Unlike {@link ALLOW_ASSISTED_DIAL}, the presence of this key further indicates that a call - * has undergone Assisted Dialing -- not just that it qualified for Assisted Dialing. - */ - public static final String IS_ASSISTED_DIALED = "android.telecom.extra.IS_ASSISTED_DIALED"; + public static final String USE_ASSISTED_DIALING = "android.telecom.extra.USE_ASSISTED_DIALING"; // TODO(erfanian): a bug Replace with the platform/telecom API when available. /** Additional information relating to the assisted dialing transformation. */ public static final String ASSISTED_DIALING_EXTRAS = "android.telecom.extra.ASSISTED_DIALING_EXTRAS"; + /** Indicates the Connection/Call used assisted dialing. */ + public static final int PROPERTY_ASSISTED_DIALING_USED = 0x00000200; + public static final String EXTRA_IS_REFRESH = BuildCompat.isAtLeastOMR1() ? "android.telephony.extra.IS_REFRESH" : "is_refresh"; diff --git a/java/com/android/dialer/precall/impl/AssistedDialAction.java b/java/com/android/dialer/precall/impl/AssistedDialAction.java index c4f61d2dd..dc2510960 100644 --- a/java/com/android/dialer/precall/impl/AssistedDialAction.java +++ b/java/com/android/dialer/precall/impl/AssistedDialAction.java @@ -51,10 +51,12 @@ public class AssistedDialAction implements PreCallAction { AssistedDialingMediator assistedDialingMediator = ConcreteCreator.createNewAssistedDialingMediator( context.getSystemService(TelephonyManager.class), context); + if (Build.VERSION.SDK_INT > ConcreteCreator.BUILD_CODE_CEILING) { + builder.getOutgoingCallExtras().putBoolean(TelephonyManagerCompat.USE_ASSISTED_DIALING, true); + } if (!assistedDialingMediator.isPlatformEligible()) { return; } - builder.getOutgoingCallExtras().putBoolean(TelephonyManagerCompat.ALLOW_ASSISTED_DIAL, true); String phoneNumber = builder.getUri().getScheme().equals(PhoneAccount.SCHEME_TEL) ? builder.getUri().getSchemeSpecificPart() @@ -62,8 +64,8 @@ public class AssistedDialAction implements PreCallAction { Optional transformedNumber = assistedDialingMediator.attemptAssistedDial(phoneNumber); if (transformedNumber.isPresent()) { + builder.getOutgoingCallExtras().putBoolean(TelephonyManagerCompat.USE_ASSISTED_DIALING, true); Bundle assistedDialingExtras = transformedNumber.get().toBundle(); - builder.getOutgoingCallExtras().putBoolean(TelephonyManagerCompat.IS_ASSISTED_DIALED, true); builder .getOutgoingCallExtras() .putBundle(TelephonyManagerCompat.ASSISTED_DIALING_EXTRAS, assistedDialingExtras); diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java index 812024904..94c79e904 100644 --- a/java/com/android/incallui/call/DialerCall.java +++ b/java/com/android/incallui/call/DialerCall.java @@ -20,6 +20,7 @@ import android.Manifest.permission; import android.content.Context; import android.hardware.camera2.CameraCharacteristics; import android.net.Uri; +import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -43,6 +44,7 @@ import android.telecom.VideoProfile; import android.text.TextUtils; import com.android.contacts.common.compat.CallCompat; import com.android.contacts.common.compat.telecom.TelecomManagerCompat; +import com.android.dialer.assisteddialing.ConcreteCreator; import com.android.dialer.assisteddialing.TransformationInfo; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentParser; @@ -1073,19 +1075,50 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa return mLogState.isIncoming; } + /** + * Try and determine if the call used assisted dialing. + * + *

      We will not be able to verify a call underwent assisted dialing until the Platform + * implmentation is complete in P+. + * + * @return a boolean indicating assisted dialing may have been performed + */ public boolean isAssistedDialed() { if (getIntentExtras() != null) { - return getIntentExtras().getBoolean(TelephonyManagerCompat.IS_ASSISTED_DIALED, false); + // O_MR1 and below uses the existence of USE_ASSISTED_DIALING to indicate assisted dialing + // was used. The Dialer client is responsible for performing assisted dialing before + // placing the outgoing call. + // + // The existence of the assisted dialing extras indicates that assisted dialing took place. + if (getIntentExtras().getBoolean(TelephonyManagerCompat.USE_ASSISTED_DIALING, false) + && getAssistedDialingExtras() != null + && Build.VERSION.SDK_INT <= ConcreteCreator.BUILD_CODE_CEILING) { + return true; + } + } + + // Starting in P+ USE_ASSISTED_DIALING indicates that the client requested the platform + // perform assisted dialing. PROPERTY_ASSISTED_DIALING_USED indicates assisted dialing took + // place. + if (hasProperty(TelephonyManagerCompat.PROPERTY_ASSISTED_DIALING_USED) + && Build.VERSION.SDK_INT > ConcreteCreator.BUILD_CODE_CEILING) { + return true; } return false; } + @Nullable public TransformationInfo getAssistedDialingExtras() { - if (isAssistedDialed()) { - return TransformationInfo.newInstanceFromBundle( - getIntentExtras().getBundle(TelephonyManagerCompat.ASSISTED_DIALING_EXTRAS)); + if (getIntentExtras() == null) { + return null; } - return null; + + if (getIntentExtras().getBundle(TelephonyManagerCompat.ASSISTED_DIALING_EXTRAS) == null) { + return null; + } + + return TransformationInfo.newInstanceFromBundle( + getIntentExtras().getBundle(TelephonyManagerCompat.ASSISTED_DIALING_EXTRAS)); } public LatencyReport getLatencyReport() { diff --git a/java/com/android/incallui/contactgrid/TopRow.java b/java/com/android/incallui/contactgrid/TopRow.java index f8a485519..556b11ba0 100644 --- a/java/com/android/incallui/contactgrid/TopRow.java +++ b/java/com/android/incallui/contactgrid/TopRow.java @@ -175,7 +175,7 @@ public class TopRow { } } - if (state.isAssistedDialed) { + if (state.isAssistedDialed && state.assistedDialingExtras != null) { LogUtil.i("TopRow.getLabelForDialing", "using assisted dialing label."); String countryCode = String.valueOf(state.assistedDialingExtras.transformedNumberCountryCallingCode()); -- cgit v1.2.3 From 01996a6113eb809d987fe031bebad4bc4530393b Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Thu, 21 Dec 2017 12:48:35 -0800 Subject: Added context menu for favorite contacts in new speed dial. Bug: 36841782 Test: n/a PiperOrigin-RevId: 179847039 Change-Id: I5006e074fff4041eef54c9b81280097ac30be5b3 --- java/com/android/dialer/app/res/values/colors.xml | 1 - .../dialer/speeddial/FavoritesViewHolder.java | 56 +++++++++++++++-- .../dialer/speeddial/SpeedDialFragment.java | 14 ++++- .../res/drawable/context_menu_background.xml | 25 ++++++++ .../speeddial/res/layout/favorite_context_menu.xml | 72 ++++++++++++++++++++++ .../speeddial/res/layout/fragment_speed_dial.xml | 4 +- .../android/dialer/speeddial/res/values/dimens.xml | 15 +++++ .../dialer/speeddial/res/values/strings.xml | 15 +++++ .../android/dialer/speeddial/res/values/styles.xml | 27 ++++++++ .../com/android/dialer/theme/res/values/dimens.xml | 3 + 10 files changed, 222 insertions(+), 10 deletions(-) create mode 100644 java/com/android/dialer/speeddial/res/drawable/context_menu_background.xml create mode 100644 java/com/android/dialer/speeddial/res/layout/favorite_context_menu.xml create mode 100644 java/com/android/dialer/speeddial/res/values/styles.xml (limited to 'java/com/android') diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml index 84a381f21..bb3662cda 100644 --- a/java/com/android/dialer/app/res/values/colors.xml +++ b/java/com/android/dialer/app/res/values/colors.xml @@ -68,7 +68,6 @@ @color/dialer_theme_color_20pct #eeeeee - #D8D8D8 @color/dialer_primary_text_color diff --git a/java/com/android/dialer/speeddial/FavoritesViewHolder.java b/java/com/android/dialer/speeddial/FavoritesViewHolder.java index f9eec8e79..4e8d4ad07 100644 --- a/java/com/android/dialer/speeddial/FavoritesViewHolder.java +++ b/java/com/android/dialer/speeddial/FavoritesViewHolder.java @@ -22,12 +22,16 @@ import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; +import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; +import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.widget.FrameLayout; +import android.widget.LinearLayout.LayoutParams; +import android.widget.PopupWindow; import android.widget.QuickContactBadge; import android.widget.TextView; import com.android.dialer.common.Assert; @@ -44,6 +48,9 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder private final TextView nameView; private final TextView phoneType; private final FrameLayout videoCallIcon; + private final View contextMenuAnchor; + + private PopupWindow contextMenu; private boolean hasDefaultNumber; private boolean isVideoCall; @@ -56,6 +63,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder nameView = view.findViewById(R.id.name); phoneType = view.findViewById(R.id.phone_type); videoCallIcon = view.findViewById(R.id.video_call_container); + contextMenuAnchor = view.findViewById(R.id.avatar_container); view.setOnClickListener(this); view.setOnLongClickListener(this); photoView.setClickable(false); @@ -112,12 +120,44 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder } @Override - public boolean onLongClick(View v) { - // TODO(calderwoodra): implement drag and drop logic - listener.onLongClick(number); + public boolean onLongClick(View view) { + Context context = itemView.getContext(); + View contentView = LayoutInflater.from(context).inflate(R.layout.favorite_context_menu, null); + contentView + .findViewById(R.id.voice_call_container) + .setOnClickListener(v -> listener.onClick(Assert.isNotNull(number), false)); + contentView + .findViewById(R.id.video_call_container) + .setOnClickListener(v -> listener.onClick(Assert.isNotNull(number), true)); + contentView + .findViewById(R.id.send_message_container) + .setOnClickListener(v -> listener.openSmsConversation(Assert.isNotNull(number))); + contentView + .findViewById(R.id.remove_container) + .setOnClickListener(v -> listener.removeFavoriteContact()); + contentView + .findViewById(R.id.contact_info_container) + .setOnClickListener(v -> listener.openContactInfo()); + + int offset = + context.getResources().getDimensionPixelSize(R.dimen.speed_dial_context_menu_x_offset); + int padding = + context.getResources().getDimensionPixelSize(R.dimen.speed_dial_context_menu_extra_width); + int width = padding + itemView.getWidth(); + int elevation = context.getResources().getDimensionPixelSize(R.dimen.context_menu_elevation); + contextMenu = new PopupWindow(contentView, width, LayoutParams.WRAP_CONTENT, true); + contextMenu.setBackgroundDrawable(context.getDrawable(R.drawable.context_menu_background)); + contextMenu.setElevation(elevation); + contextMenu.setOnDismissListener(() -> contextMenu = null); + contextMenu.showAsDropDown(contextMenuAnchor, offset, 0); return true; } + @VisibleForTesting + public PopupWindow getContextMenu() { + return contextMenu; + } + /** Listener/callback for {@link FavoritesViewHolder} actions. */ public interface FavoriteContactsListener { @@ -127,7 +167,13 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder /** Called when the user clicks on a favorite contact. */ void onClick(String number, boolean isVideoCall); - /** Called when the user long clicks on a favorite contact. */ - void onLongClick(String number); + /** Called when the user selects send message from the context menu. */ + void openSmsConversation(String number); + + /** Called when the user selects remove from the context menu. */ + void removeFavoriteContact(); + + /** Called when the user selects contact info from the context menu. */ + void openContactInfo(); } } diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java index 979c894fe..a75c5ddbd 100644 --- a/java/com/android/dialer/speeddial/SpeedDialFragment.java +++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java @@ -112,8 +112,18 @@ public class SpeedDialFragment extends Fragment { } @Override - public void onLongClick(String number) { - // TODO(calderwoodra): show favorite contact floating context menu + public void openSmsConversation(String number) { + // TODO(calderwoodra): open sms conversation + } + + @Override + public void removeFavoriteContact() { + // TODO(calderwoodra): remove contact from favorites + } + + @Override + public void openContactInfo() { + // TODO(calderwoodra): open quick contact info } } diff --git a/java/com/android/dialer/speeddial/res/drawable/context_menu_background.xml b/java/com/android/dialer/speeddial/res/drawable/context_menu_background.xml new file mode 100644 index 000000000..fd60757b5 --- /dev/null +++ b/java/com/android/dialer/speeddial/res/drawable/context_menu_background.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/java/com/android/dialer/speeddial/res/layout/favorite_context_menu.xml b/java/com/android/dialer/speeddial/res/layout/favorite_context_menu.xml new file mode 100644 index 000000000..04d0139e0 --- /dev/null +++ b/java/com/android/dialer/speeddial/res/layout/favorite_context_menu.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml b/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml index d432f097b..b4fc9c08d 100644 --- a/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml +++ b/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml @@ -19,6 +19,6 @@ android:id="@+id/speed_dial_recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingStart="16dp" - android:paddingEnd="16dp" + android:paddingStart="@dimen/speed_dial_recyclerview_horizontal_padding" + android:paddingEnd="@dimen/speed_dial_recyclerview_horizontal_padding" android:clipToPadding="false"/> diff --git a/java/com/android/dialer/speeddial/res/values/dimens.xml b/java/com/android/dialer/speeddial/res/values/dimens.xml index 74b509b73..ce2de9dcc 100644 --- a/java/com/android/dialer/speeddial/res/values/dimens.xml +++ b/java/com/android/dialer/speeddial/res/values/dimens.xml @@ -16,4 +16,19 @@ --> 280dp + 200dp + 4dp + 16dp + + + 28dp + + -24dp + 16dp \ No newline at end of file diff --git a/java/com/android/dialer/speeddial/res/values/strings.xml b/java/com/android/dialer/speeddial/res/values/strings.xml index 677f772c5..666eedea9 100644 --- a/java/com/android/dialer/speeddial/res/values/strings.xml +++ b/java/com/android/dialer/speeddial/res/values/strings.xml @@ -38,6 +38,21 @@ Call + + Voice Call + + + Video Call + + + Message + + + Remove + + + Contact info + Add Favorite \ No newline at end of file diff --git a/java/com/android/dialer/speeddial/res/values/styles.xml b/java/com/android/dialer/speeddial/res/values/styles.xml new file mode 100644 index 000000000..83bbd09e5 --- /dev/null +++ b/java/com/android/dialer/speeddial/res/values/styles.xml @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/java/com/android/dialer/theme/res/values/dimens.xml b/java/com/android/dialer/theme/res/values/dimens.xml index 2b5243ebd..52a7560b3 100644 --- a/java/com/android/dialer/theme/res/values/dimens.xml +++ b/java/com/android/dialer/theme/res/values/dimens.xml @@ -50,4 +50,7 @@ 72dp + + + #D8D8D8 -- cgit v1.2.3 From 507340c5b86799320454811b3fca1b8534028fb1 Mon Sep 17 00:00:00 2001 From: linyuh Date: Thu, 21 Dec 2017 12:51:18 -0800 Subject: Use the orientation obtained in onFinishInflate as the truth in DialpadView. Bug: 69665429 Test: Manual PiperOrigin-RevId: 179847326 Change-Id: Iaf40910dd4692bbed73d2496ffd27f60d2e28307 --- .../android/dialer/dialpadview/DialpadView.java | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/dialpadview/DialpadView.java b/java/com/android/dialer/dialpadview/DialpadView.java index 2f494e49d..7b95ba7ea 100644 --- a/java/com/android/dialer/dialpadview/DialpadView.java +++ b/java/com/android/dialer/dialpadview/DialpadView.java @@ -87,6 +87,7 @@ public class DialpadView extends LinearLayout { private ViewGroup mRateContainer; private TextView mIldCountry; private TextView mIldRate; + private boolean mIsLandscapeMode; public DialpadView(Context context) { this(context, null); @@ -125,6 +126,12 @@ public class DialpadView extends LinearLayout { protected void onFinishInflate() { super.onFinishInflate(); + // The orientation obtained at this point should be used as the only truth for DialpadView as we + // observed inconsistency between configurations obtained here and in + // OnPreDrawListenerForKeyLayoutAdjust under rare circumstances. + mIsLandscapeMode = + (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); + setupKeypad(); mDigits = (EditText) findViewById(R.id.digits); mDelete = (ImageButton) findViewById(R.id.deleteButton); @@ -281,7 +288,7 @@ public class DialpadView extends LinearLayout { final DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(BUTTON_IDS[i]); ViewPropertyAnimator animator = dialpadKey.animate(); - if (isLandscapeMode()) { + if (mIsLandscapeMode) { // Landscape orientation requires translation along the X axis. // For RTL locales, ensure we translate negative on the X axis. dialpadKey.setTranslationX((mIsRtl ? -1 : 1) * mTranslateDistance); @@ -320,7 +327,7 @@ public class DialpadView extends LinearLayout { * @return The animation delay. */ private int getKeyButtonAnimationDelay(int buttonId) { - if (isLandscapeMode()) { + if (mIsLandscapeMode) { if (mIsRtl) { if (buttonId == R.id.three) { return KEY_FRAME_DURATION * 1; @@ -408,7 +415,7 @@ public class DialpadView extends LinearLayout { * @return The animation duration. */ private int getKeyButtonAnimationDuration(int buttonId) { - if (isLandscapeMode()) { + if (mIsLandscapeMode) { if (mIsRtl) { if (buttonId == R.id.one || buttonId == R.id.four @@ -463,10 +470,6 @@ public class DialpadView extends LinearLayout { return 0; } - private boolean isLandscapeMode() { - return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; - } - /** * An {@link OnPreDrawListener} that adjusts the height/width of each key layout so that they can * be properly aligned. @@ -525,7 +528,7 @@ public class DialpadView extends LinearLayout { } private boolean shouldAdjustKeySizes() { - return isLandscapeMode() ? shouldAdjustKeyWidths() : shouldAdjustDigitKeyHeights(); + return mIsLandscapeMode ? shouldAdjustKeyWidths() : shouldAdjustDigitKeyHeights(); } /** @@ -533,7 +536,7 @@ public class DialpadView extends LinearLayout { * device is in landscape mode. */ private boolean shouldAdjustKeyWidths() { - Assert.checkState(isLandscapeMode()); + Assert.checkState(mIsLandscapeMode); DialpadKeyButton dialpadKeyButton = (DialpadKeyButton) findViewById(BUTTON_IDS[0]); LinearLayout keyLayout = @@ -556,7 +559,7 @@ public class DialpadView extends LinearLayout { * called when the device is in portrait mode. */ private boolean shouldAdjustDigitKeyHeights() { - Assert.checkState(!isLandscapeMode()); + Assert.checkState(!mIsLandscapeMode); DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(BUTTON_IDS[0]); LinearLayout keyLayout = (LinearLayout) dialpadKey.findViewById(R.id.dialpad_key_layout); @@ -576,7 +579,7 @@ public class DialpadView extends LinearLayout { } private void adjustKeySizes() { - if (isLandscapeMode()) { + if (mIsLandscapeMode) { adjustKeyWidths(); } else { adjustDigitKeyHeights(); @@ -594,7 +597,7 @@ public class DialpadView extends LinearLayout { * LinearLayout#setLayoutParams(ViewGroup.LayoutParams)}. */ private void adjustDigitKeyHeights() { - Assert.checkState(!isLandscapeMode()); + Assert.checkState(!mIsLandscapeMode); int maxHeight = 0; @@ -638,7 +641,7 @@ public class DialpadView extends LinearLayout { * View#setLayoutParams(ViewGroup.LayoutParams)}. */ private void adjustKeyWidths() { - Assert.checkState(isLandscapeMode()); + Assert.checkState(mIsLandscapeMode); int maxWidth = 0; for (int buttonId : BUTTON_IDS) { -- cgit v1.2.3 From bf4bb0555ca827e660ad05b4caf982a030211c03 Mon Sep 17 00:00:00 2001 From: linyuh Date: Thu, 21 Dec 2017 15:42:00 -0800 Subject: Reorganize classes related to smart dial. Bug: 30215380,70633239 Test: Existing tests PiperOrigin-RevId: 179868033 Change-Id: If8cdbdfafb3a66397623578131649cb8adc18733 --- java/com/android/dialer/app/DialtactsActivity.java | 4 +- .../app/list/SmartDialNumberListAdapter.java | 6 +- .../dialer/app/list/SmartDialSearchFragment.java | 2 +- .../dialer/database/DialerDatabaseHelper.java | 4 +- .../dialer/dialpadview/SmartDialCursorLoader.java | 182 ----- .../cp2/SearchContactsCursorLoader.java | 2 +- .../dialer/smartdial/BulgarianSmartDialMap.java | 91 --- .../dialer/smartdial/CompositeSmartDialMap.java | 172 ----- .../dialer/smartdial/LatinSmartDialMap.java | 785 --------------------- .../dialer/smartdial/RussianSmartDialMap.java | 94 --- .../dialer/smartdial/SmartDialCursorLoader.java | 182 +++++ .../com/android/dialer/smartdial/SmartDialMap.java | 103 --- .../dialer/smartdial/SmartDialMatchPosition.java | 70 -- .../dialer/smartdial/SmartDialNameMatcher.java | 429 ----------- .../android/dialer/smartdial/SmartDialPrefix.java | 601 ---------------- .../dialer/smartdial/UkrainianSmartDialMap.java | 93 --- .../smartdial/map/BulgarianSmartDialMap.java | 91 +++ .../smartdial/map/CompositeSmartDialMap.java | 172 +++++ .../dialer/smartdial/map/LatinSmartDialMap.java | 785 +++++++++++++++++++++ .../dialer/smartdial/map/RussianSmartDialMap.java | 94 +++ .../android/dialer/smartdial/map/SmartDialMap.java | 103 +++ .../smartdial/map/UkrainianSmartDialMap.java | 93 +++ .../smartdial/util/SmartDialMatchPosition.java | 70 ++ .../smartdial/util/SmartDialNameMatcher.java | 430 +++++++++++ .../dialer/smartdial/util/SmartDialPrefix.java | 602 ++++++++++++++++ 25 files changed, 2631 insertions(+), 2629 deletions(-) delete mode 100644 java/com/android/dialer/dialpadview/SmartDialCursorLoader.java delete mode 100644 java/com/android/dialer/smartdial/BulgarianSmartDialMap.java delete mode 100644 java/com/android/dialer/smartdial/CompositeSmartDialMap.java delete mode 100644 java/com/android/dialer/smartdial/LatinSmartDialMap.java delete mode 100644 java/com/android/dialer/smartdial/RussianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/SmartDialCursorLoader.java delete mode 100644 java/com/android/dialer/smartdial/SmartDialMap.java delete mode 100644 java/com/android/dialer/smartdial/SmartDialMatchPosition.java delete mode 100644 java/com/android/dialer/smartdial/SmartDialNameMatcher.java delete mode 100644 java/com/android/dialer/smartdial/SmartDialPrefix.java delete mode 100644 java/com/android/dialer/smartdial/UkrainianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/CompositeSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/LatinSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/RussianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/SmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/util/SmartDialMatchPosition.java create mode 100644 java/com/android/dialer/smartdial/util/SmartDialNameMatcher.java create mode 100644 java/com/android/dialer/smartdial/util/SmartDialPrefix.java (limited to 'java/com/android') diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java index eb95a4ee4..bbb45943a 100644 --- a/java/com/android/dialer/app/DialtactsActivity.java +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -133,8 +133,8 @@ import com.android.dialer.searchfragment.list.NewSearchFragment; import com.android.dialer.searchfragment.list.NewSearchFragment.SearchFragmentListener; import com.android.dialer.simulator.Simulator; import com.android.dialer.simulator.SimulatorComponent; -import com.android.dialer.smartdial.SmartDialNameMatcher; -import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.smartdial.util.SmartDialNameMatcher; +import com.android.dialer.smartdial.util.SmartDialPrefix; import com.android.dialer.storage.StorageComponent; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.DialerUtils; diff --git a/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java index 5b48ccfd0..1d2cda3ea 100644 --- a/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java +++ b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java @@ -22,9 +22,9 @@ import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import com.android.contacts.common.list.ContactListItemView; import com.android.dialer.common.LogUtil; -import com.android.dialer.dialpadview.SmartDialCursorLoader; -import com.android.dialer.smartdial.SmartDialMatchPosition; -import com.android.dialer.smartdial.SmartDialNameMatcher; +import com.android.dialer.smartdial.SmartDialCursorLoader; +import com.android.dialer.smartdial.util.SmartDialMatchPosition; +import com.android.dialer.smartdial.util.SmartDialNameMatcher; import com.android.dialer.util.CallUtil; import java.util.ArrayList; diff --git a/java/com/android/dialer/app/list/SmartDialSearchFragment.java b/java/com/android/dialer/app/list/SmartDialSearchFragment.java index e97a16c19..1a7f19515 100644 --- a/java/com/android/dialer/app/list/SmartDialSearchFragment.java +++ b/java/com/android/dialer/app/list/SmartDialSearchFragment.java @@ -31,7 +31,7 @@ import com.android.dialer.app.R; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.common.LogUtil; import com.android.dialer.database.DialerDatabaseHelper; -import com.android.dialer.dialpadview.SmartDialCursorLoader; +import com.android.dialer.smartdial.SmartDialCursorLoader; import com.android.dialer.util.PermissionsUtil; import com.android.dialer.widget.EmptyContentView; import java.util.Arrays; diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java index b0bd62a34..3fb87304b 100644 --- a/java/com/android/dialer/database/DialerDatabaseHelper.java +++ b/java/com/android/dialer/database/DialerDatabaseHelper.java @@ -42,8 +42,8 @@ import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor.Worker; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; -import com.android.dialer.smartdial.SmartDialNameMatcher; -import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.smartdial.util.SmartDialNameMatcher; +import com.android.dialer.smartdial.util.SmartDialPrefix; import com.android.dialer.util.PermissionsUtil; import java.util.ArrayList; import java.util.HashSet; diff --git a/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java b/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java deleted file mode 100644 index d085b55bd..000000000 --- a/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2013 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.AsyncTaskLoader; -import android.content.Context; -import android.database.Cursor; -import android.database.MatrixCursor; -import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; -import com.android.dialer.common.LogUtil; -import com.android.dialer.database.Database; -import com.android.dialer.database.DialerDatabaseHelper; -import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; -import com.android.dialer.smartdial.SmartDialNameMatcher; -import com.android.dialer.util.PermissionsUtil; -import java.util.ArrayList; - -/** Implements a Loader class to asynchronously load SmartDial search results. */ -public class SmartDialCursorLoader extends AsyncTaskLoader { - - private static final String TAG = "SmartDialCursorLoader"; - private static final boolean DEBUG = false; - - private final Context mContext; - - private Cursor mCursor; - - private String mQuery; - private SmartDialNameMatcher mNameMatcher; - - private boolean mShowEmptyListForNullQuery = true; - - public SmartDialCursorLoader(Context context) { - super(context); - mContext = context; - } - - /** - * Configures the query string to be used to find SmartDial matches. - * - * @param query The query string user typed. - */ - public void configureQuery(String query) { - if (DEBUG) { - LogUtil.v(TAG, "Configure new query to be " + query); - } - mQuery = SmartDialNameMatcher.normalizeNumber(mContext, query); - - /** Constructs a name matcher object for matching names. */ - mNameMatcher = new SmartDialNameMatcher(mQuery); - mNameMatcher.setShouldMatchEmptyQuery(!mShowEmptyListForNullQuery); - } - - /** - * Queries the SmartDial database and loads results in background. - * - * @return Cursor of contacts that matches the SmartDial query. - */ - @Override - public Cursor loadInBackground() { - if (DEBUG) { - LogUtil.v(TAG, "Load in background " + mQuery); - } - - if (!PermissionsUtil.hasContactsReadPermissions(mContext)) { - return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); - } - - /** Loads results from the database helper. */ - final DialerDatabaseHelper dialerDatabaseHelper = - Database.get(mContext).getDatabaseHelper(mContext); - final ArrayList allMatches = - dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher); - - if (DEBUG) { - LogUtil.v(TAG, "Loaded matches " + allMatches.size()); - } - - /** Constructs a cursor for the returned array of results. */ - final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); - Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; - for (ContactNumber contact : allMatches) { - row[PhoneQuery.PHONE_ID] = contact.dataId; - row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; - row[PhoneQuery.CONTACT_ID] = contact.id; - row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; - row[PhoneQuery.PHOTO_ID] = contact.photoId; - row[PhoneQuery.DISPLAY_NAME] = contact.displayName; - row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence; - cursor.addRow(row); - } - return cursor; - } - - @Override - public void deliverResult(Cursor cursor) { - if (isReset()) { - /** The Loader has been reset; ignore the result and invalidate the data. */ - releaseResources(cursor); - return; - } - - /** Hold a reference to the old data so it doesn't get garbage collected. */ - Cursor oldCursor = mCursor; - mCursor = cursor; - - if (isStarted()) { - /** If the Loader is in a started state, deliver the results to the client. */ - super.deliverResult(cursor); - } - - /** Invalidate the old data as we don't need it any more. */ - if (oldCursor != null && oldCursor != cursor) { - releaseResources(oldCursor); - } - } - - @Override - protected void onStartLoading() { - if (mCursor != null) { - /** Deliver any previously loaded data immediately. */ - deliverResult(mCursor); - } - if (mCursor == null) { - /** Force loads every time as our results change with queries. */ - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ - cancelLoad(); - } - - @Override - protected void onReset() { - /** Ensure the loader has been stopped. */ - onStopLoading(); - - /** Release all previously saved query results. */ - if (mCursor != null) { - releaseResources(mCursor); - mCursor = null; - } - } - - @Override - public void onCanceled(Cursor cursor) { - super.onCanceled(cursor); - - /** The load has been canceled, so we should release the resources associated with 'data'. */ - releaseResources(cursor); - } - - private void releaseResources(Cursor cursor) { - if (cursor != null) { - cursor.close(); - } - } - - public void setShowEmptyListForNullQuery(boolean show) { - mShowEmptyListForNullQuery = show; - if (mNameMatcher != null) { - mNameMatcher.setShouldMatchEmptyQuery(!show); - } - } -} diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java index 23e3f9d88..23f368f54 100644 --- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java +++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java @@ -28,9 +28,9 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.contacts.common.preference.ContactsPreferences; -import com.android.dialer.dialpadview.SmartDialCursorLoader; import com.android.dialer.searchfragment.common.Projections; import com.android.dialer.searchfragment.common.SearchCursor; +import com.android.dialer.smartdial.SmartDialCursorLoader; /** Cursor Loader for CP2 contacts. */ public final class SearchContactsCursorLoader extends CursorLoader { diff --git a/java/com/android/dialer/smartdial/BulgarianSmartDialMap.java b/java/com/android/dialer/smartdial/BulgarianSmartDialMap.java deleted file mode 100644 index a16d15919..000000000 --- a/java/com/android/dialer/smartdial/BulgarianSmartDialMap.java +++ /dev/null @@ -1,91 +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.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** A {@link SmartDialMap} for the Bulgarian alphabet. */ -@SuppressWarnings("Guava") -final class BulgarianSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - // Reference: https://en.wikipedia.org/wiki/Bulgarian_alphabet - static { - CHAR_TO_KEY_MAP.put('а', '2'); - CHAR_TO_KEY_MAP.put('б', '2'); - CHAR_TO_KEY_MAP.put('в', '2'); - CHAR_TO_KEY_MAP.put('г', '2'); - - CHAR_TO_KEY_MAP.put('д', '3'); - CHAR_TO_KEY_MAP.put('е', '3'); - CHAR_TO_KEY_MAP.put('ж', '3'); - CHAR_TO_KEY_MAP.put('з', '3'); - - CHAR_TO_KEY_MAP.put('и', '4'); - CHAR_TO_KEY_MAP.put('й', '4'); - CHAR_TO_KEY_MAP.put('к', '4'); - CHAR_TO_KEY_MAP.put('л', '4'); - - CHAR_TO_KEY_MAP.put('м', '5'); - CHAR_TO_KEY_MAP.put('н', '5'); - CHAR_TO_KEY_MAP.put('о', '5'); - - CHAR_TO_KEY_MAP.put('п', '6'); - CHAR_TO_KEY_MAP.put('р', '6'); - CHAR_TO_KEY_MAP.put('с', '6'); - - CHAR_TO_KEY_MAP.put('т', '7'); - CHAR_TO_KEY_MAP.put('у', '7'); - CHAR_TO_KEY_MAP.put('ф', '7'); - CHAR_TO_KEY_MAP.put('х', '7'); - - CHAR_TO_KEY_MAP.put('ц', '8'); - CHAR_TO_KEY_MAP.put('ч', '8'); - CHAR_TO_KEY_MAP.put('ш', '8'); - CHAR_TO_KEY_MAP.put('щ', '8'); - - CHAR_TO_KEY_MAP.put('ъ', '9'); - CHAR_TO_KEY_MAP.put('ь', '9'); - CHAR_TO_KEY_MAP.put('ю', '9'); - CHAR_TO_KEY_MAP.put('я', '9'); - } - - private static BulgarianSmartDialMap instance; - - static BulgarianSmartDialMap getInstance() { - if (instance == null) { - instance = new BulgarianSmartDialMap(); - } - - return instance; - } - - private BulgarianSmartDialMap() {} - - @Override - Optional normalizeCharacter(char ch) { - ch = Character.toLowerCase(ch); - return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); - } - - @Override - SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; - } -} diff --git a/java/com/android/dialer/smartdial/CompositeSmartDialMap.java b/java/com/android/dialer/smartdial/CompositeSmartDialMap.java deleted file mode 100644 index e9f237f01..000000000 --- a/java/com/android/dialer/smartdial/CompositeSmartDialMap.java +++ /dev/null @@ -1,172 +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.smartdial; - -import android.content.Context; -import android.support.annotation.VisibleForTesting; -import android.support.v4.util.SimpleArrayMap; -import com.android.dialer.compat.CompatUtils; -import com.android.dialer.configprovider.ConfigProviderBindings; -import com.google.common.base.Optional; - -/** - * A utility class that combines the functionality of two implementations of {@link SmartDialMap} so - * that we support smart dial for dual alphabets. - * - *

      Of the two implementations of {@link SmartDialMap}, the default one always takes precedence. - * The second one is consulted only when the default one is unable to provide a valid result. - * - *

      Note that the second implementation can be absent if it is not defined for the system's 1st - * language preference. - */ -@SuppressWarnings("Guava") -public class CompositeSmartDialMap { - @VisibleForTesting - public static final String FLAG_ENABLE_DUAL_ALPHABETS = "enable_dual_alphabets_on_t9"; - - private static final SmartDialMap DEFAULT_MAP = LatinSmartDialMap.getInstance(); - - // A map in which each key is an ISO 639-2 language code and the corresponding value is a - // SmartDialMap - private static final SimpleArrayMap EXTRA_MAPS = new SimpleArrayMap<>(); - - static { - EXTRA_MAPS.put("bul", BulgarianSmartDialMap.getInstance()); - EXTRA_MAPS.put("rus", RussianSmartDialMap.getInstance()); - EXTRA_MAPS.put("ukr", UkrainianSmartDialMap.getInstance()); - } - - private CompositeSmartDialMap() {} - - /** - * Returns true if the provided character can be mapped to a key on the dialpad. - * - *

      The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - */ - static boolean isValidDialpadCharacter(Context context, char ch) { - if (DEFAULT_MAP.isValidDialpadCharacter(ch)) { - return true; - } - - Optional extraMap = getExtraMap(context); - return extraMap.isPresent() && extraMap.get().isValidDialpadCharacter(ch); - } - - /** - * Returns true if the provided character is a letter, and can be mapped to a key on the dialpad. - * - *

      The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - */ - static boolean isValidDialpadAlphabeticChar(Context context, char ch) { - if (DEFAULT_MAP.isValidDialpadAlphabeticChar(ch)) { - return true; - } - - Optional extraMap = getExtraMap(context); - return extraMap.isPresent() && extraMap.get().isValidDialpadAlphabeticChar(ch); - } - - /** - * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad. - */ - static boolean isValidDialpadNumericChar(Context context, char ch) { - if (DEFAULT_MAP.isValidDialpadNumericChar(ch)) { - return true; - } - - Optional extraMap = getExtraMap(context); - return extraMap.isPresent() && extraMap.get().isValidDialpadNumericChar(ch); - } - - /** - * Get the index of the key on the dialpad which the character corresponds to. - * - *

      The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - * - *

      If the provided character can't be mapped to a key on the dialpad, return -1. - */ - static byte getDialpadIndex(Context context, char ch) { - Optional dialpadIndex = DEFAULT_MAP.getDialpadIndex(ch); - if (dialpadIndex.isPresent()) { - return dialpadIndex.get(); - } - - Optional extraMap = getExtraMap(context); - if (extraMap.isPresent()) { - dialpadIndex = extraMap.get().getDialpadIndex(ch); - } - - return dialpadIndex.isPresent() ? dialpadIndex.get() : -1; - } - - /** - * Get the actual numeric character on the dialpad which the character corresponds to. - * - *

      The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - * - *

      If the provided character can't be mapped to a key on the dialpad, return the character. - */ - static char getDialpadNumericCharacter(Context context, char ch) { - Optional dialpadNumericChar = DEFAULT_MAP.getDialpadNumericCharacter(ch); - if (dialpadNumericChar.isPresent()) { - return dialpadNumericChar.get(); - } - - Optional extraMap = getExtraMap(context); - if (extraMap.isPresent()) { - dialpadNumericChar = extraMap.get().getDialpadNumericCharacter(ch); - } - - return dialpadNumericChar.isPresent() ? dialpadNumericChar.get() : ch; - } - - /** - * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents - * from accented characters. - * - *

      If the provided character can't be mapped to a key on the dialpad, return the character. - */ - static char normalizeCharacter(Context context, char ch) { - Optional normalizedChar = DEFAULT_MAP.normalizeCharacter(ch); - if (normalizedChar.isPresent()) { - return normalizedChar.get(); - } - - Optional extraMap = getExtraMap(context); - if (extraMap.isPresent()) { - normalizedChar = extraMap.get().normalizeCharacter(ch); - } - - return normalizedChar.isPresent() ? normalizedChar.get() : ch; - } - - @VisibleForTesting - static Optional getExtraMap(Context context) { - if (!ConfigProviderBindings.get(context).getBoolean(FLAG_ENABLE_DUAL_ALPHABETS, false)) { - return Optional.absent(); - } - - String languageCode = CompatUtils.getLocale(context).getISO3Language(); - return EXTRA_MAPS.containsKey(languageCode) - ? Optional.of(EXTRA_MAPS.get(languageCode)) - : Optional.absent(); - } -} diff --git a/java/com/android/dialer/smartdial/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/LatinSmartDialMap.java deleted file mode 100644 index b67901bbe..000000000 --- a/java/com/android/dialer/smartdial/LatinSmartDialMap.java +++ /dev/null @@ -1,785 +0,0 @@ -/* - * Copyright (C) 2016 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.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** A {@link SmartDialMap} for the Latin alphabet, which is for T9 dialpad searching. */ -@SuppressWarnings("Guava") -final class LatinSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - static { - CHAR_TO_KEY_MAP.put('a', '2'); - CHAR_TO_KEY_MAP.put('b', '2'); - CHAR_TO_KEY_MAP.put('c', '2'); - - CHAR_TO_KEY_MAP.put('d', '3'); - CHAR_TO_KEY_MAP.put('e', '3'); - CHAR_TO_KEY_MAP.put('f', '3'); - - CHAR_TO_KEY_MAP.put('g', '4'); - CHAR_TO_KEY_MAP.put('h', '4'); - CHAR_TO_KEY_MAP.put('i', '4'); - - CHAR_TO_KEY_MAP.put('j', '5'); - CHAR_TO_KEY_MAP.put('k', '5'); - CHAR_TO_KEY_MAP.put('l', '5'); - - CHAR_TO_KEY_MAP.put('m', '6'); - CHAR_TO_KEY_MAP.put('n', '6'); - CHAR_TO_KEY_MAP.put('o', '6'); - - CHAR_TO_KEY_MAP.put('p', '7'); - CHAR_TO_KEY_MAP.put('q', '7'); - CHAR_TO_KEY_MAP.put('r', '7'); - CHAR_TO_KEY_MAP.put('s', '7'); - - CHAR_TO_KEY_MAP.put('t', '8'); - CHAR_TO_KEY_MAP.put('u', '8'); - CHAR_TO_KEY_MAP.put('v', '8'); - - CHAR_TO_KEY_MAP.put('w', '9'); - CHAR_TO_KEY_MAP.put('x', '9'); - CHAR_TO_KEY_MAP.put('y', '9'); - CHAR_TO_KEY_MAP.put('z', '9'); - } - - private static LatinSmartDialMap instance; - - static LatinSmartDialMap getInstance() { - if (instance == null) { - instance = new LatinSmartDialMap(); - } - - return instance; - } - - private LatinSmartDialMap() {} - - /* - * The switch statement in this function was generated using the python code: - * from unidecode import unidecode - * for i in range(192, 564): - * char = unichr(i) - * decoded = unidecode(char) - * # Unicode characters that decompose into multiple characters i.e. - * # into ss are not supported for now - * if (len(decoded) == 1 and decoded.isalpha()): - * print "case '" + char + "': return Optional.of('" + unidecode(char) + "');" - * - * This gives us a way to map characters containing accents/diacritics to their - * alphabetic equivalents. The unidecode library can be found at: - * http://pypi.python.org/pypi/Unidecode/0.04.1 - * - * Also remaps all upper case latin characters to their lower case equivalents. - */ - @Override - Optional normalizeCharacter(char ch) { - if (isValidDialpadAlphabeticChar(ch)) { - return Optional.of(ch); - } - - switch (ch) { - case 'À': - return Optional.of('a'); - case 'Á': - return Optional.of('a'); - case 'Â': - return Optional.of('a'); - case 'Ã': - return Optional.of('a'); - case 'Ä': - return Optional.of('a'); - case 'Å': - return Optional.of('a'); - case 'Ç': - return Optional.of('c'); - case 'È': - return Optional.of('e'); - case 'É': - return Optional.of('e'); - case 'Ê': - return Optional.of('e'); - case 'Ë': - return Optional.of('e'); - case 'Ì': - return Optional.of('i'); - case 'Í': - return Optional.of('i'); - case 'Î': - return Optional.of('i'); - case 'Ï': - return Optional.of('i'); - case 'Ð': - return Optional.of('d'); - case 'Ñ': - return Optional.of('n'); - case 'Ò': - return Optional.of('o'); - case 'Ó': - return Optional.of('o'); - case 'Ô': - return Optional.of('o'); - case 'Õ': - return Optional.of('o'); - case 'Ö': - return Optional.of('o'); - case '×': - return Optional.of('x'); - case 'Ø': - return Optional.of('o'); - case 'Ù': - return Optional.of('u'); - case 'Ú': - return Optional.of('u'); - case 'Û': - return Optional.of('u'); - case 'Ü': - return Optional.of('u'); - case 'Ý': - return Optional.of('u'); - case 'à': - return Optional.of('a'); - case 'á': - return Optional.of('a'); - case 'â': - return Optional.of('a'); - case 'ã': - return Optional.of('a'); - case 'ä': - return Optional.of('a'); - case 'å': - return Optional.of('a'); - case 'ç': - return Optional.of('c'); - case 'è': - return Optional.of('e'); - case 'é': - return Optional.of('e'); - case 'ê': - return Optional.of('e'); - case 'ë': - return Optional.of('e'); - case 'ì': - return Optional.of('i'); - case 'í': - return Optional.of('i'); - case 'î': - return Optional.of('i'); - case 'ï': - return Optional.of('i'); - case 'ð': - return Optional.of('d'); - case 'ñ': - return Optional.of('n'); - case 'ò': - return Optional.of('o'); - case 'ó': - return Optional.of('o'); - case 'ô': - return Optional.of('o'); - case 'õ': - return Optional.of('o'); - case 'ö': - return Optional.of('o'); - case 'ø': - return Optional.of('o'); - case 'ù': - return Optional.of('u'); - case 'ú': - return Optional.of('u'); - case 'û': - return Optional.of('u'); - case 'ü': - return Optional.of('u'); - case 'ý': - return Optional.of('y'); - case 'ÿ': - return Optional.of('y'); - case 'Ā': - return Optional.of('a'); - case 'ā': - return Optional.of('a'); - case 'Ă': - return Optional.of('a'); - case 'ă': - return Optional.of('a'); - case 'Ą': - return Optional.of('a'); - case 'ą': - return Optional.of('a'); - case 'Ć': - return Optional.of('c'); - case 'ć': - return Optional.of('c'); - case 'Ĉ': - return Optional.of('c'); - case 'ĉ': - return Optional.of('c'); - case 'Ċ': - return Optional.of('c'); - case 'ċ': - return Optional.of('c'); - case 'Č': - return Optional.of('c'); - case 'č': - return Optional.of('c'); - case 'Ď': - return Optional.of('d'); - case 'ď': - return Optional.of('d'); - case 'Đ': - return Optional.of('d'); - case 'đ': - return Optional.of('d'); - case 'Ē': - return Optional.of('e'); - case 'ē': - return Optional.of('e'); - case 'Ĕ': - return Optional.of('e'); - case 'ĕ': - return Optional.of('e'); - case 'Ė': - return Optional.of('e'); - case 'ė': - return Optional.of('e'); - case 'Ę': - return Optional.of('e'); - case 'ę': - return Optional.of('e'); - case 'Ě': - return Optional.of('e'); - case 'ě': - return Optional.of('e'); - case 'Ĝ': - return Optional.of('g'); - case 'ĝ': - return Optional.of('g'); - case 'Ğ': - return Optional.of('g'); - case 'ğ': - return Optional.of('g'); - case 'Ġ': - return Optional.of('g'); - case 'ġ': - return Optional.of('g'); - case 'Ģ': - return Optional.of('g'); - case 'ģ': - return Optional.of('g'); - case 'Ĥ': - return Optional.of('h'); - case 'ĥ': - return Optional.of('h'); - case 'Ħ': - return Optional.of('h'); - case 'ħ': - return Optional.of('h'); - case 'Ĩ': - return Optional.of('i'); - case 'ĩ': - return Optional.of('i'); - case 'Ī': - return Optional.of('i'); - case 'ī': - return Optional.of('i'); - case 'Ĭ': - return Optional.of('i'); - case 'ĭ': - return Optional.of('i'); - case 'Į': - return Optional.of('i'); - case 'į': - return Optional.of('i'); - case 'İ': - return Optional.of('i'); - case 'ı': - return Optional.of('i'); - case 'Ĵ': - return Optional.of('j'); - case 'ĵ': - return Optional.of('j'); - case 'Ķ': - return Optional.of('k'); - case 'ķ': - return Optional.of('k'); - case 'ĸ': - return Optional.of('k'); - case 'Ĺ': - return Optional.of('l'); - case 'ĺ': - return Optional.of('l'); - case 'Ļ': - return Optional.of('l'); - case 'ļ': - return Optional.of('l'); - case 'Ľ': - return Optional.of('l'); - case 'ľ': - return Optional.of('l'); - case 'Ŀ': - return Optional.of('l'); - case 'ŀ': - return Optional.of('l'); - case 'Ł': - return Optional.of('l'); - case 'ł': - return Optional.of('l'); - case 'Ń': - return Optional.of('n'); - case 'ń': - return Optional.of('n'); - case 'Ņ': - return Optional.of('n'); - case 'ņ': - return Optional.of('n'); - case 'Ň': - return Optional.of('n'); - case 'ň': - return Optional.of('n'); - case 'Ō': - return Optional.of('o'); - case 'ō': - return Optional.of('o'); - case 'Ŏ': - return Optional.of('o'); - case 'ŏ': - return Optional.of('o'); - case 'Ő': - return Optional.of('o'); - case 'ő': - return Optional.of('o'); - case 'Ŕ': - return Optional.of('r'); - case 'ŕ': - return Optional.of('r'); - case 'Ŗ': - return Optional.of('r'); - case 'ŗ': - return Optional.of('r'); - case 'Ř': - return Optional.of('r'); - case 'ř': - return Optional.of('r'); - case 'Ś': - return Optional.of('s'); - case 'ś': - return Optional.of('s'); - case 'Ŝ': - return Optional.of('s'); - case 'ŝ': - return Optional.of('s'); - case 'Ş': - return Optional.of('s'); - case 'ş': - return Optional.of('s'); - case 'Š': - return Optional.of('s'); - case 'š': - return Optional.of('s'); - case 'Ţ': - return Optional.of('t'); - case 'ţ': - return Optional.of('t'); - case 'Ť': - return Optional.of('t'); - case 'ť': - return Optional.of('t'); - case 'Ŧ': - return Optional.of('t'); - case 'ŧ': - return Optional.of('t'); - case 'Ũ': - return Optional.of('u'); - case 'ũ': - return Optional.of('u'); - case 'Ū': - return Optional.of('u'); - case 'ū': - return Optional.of('u'); - case 'Ŭ': - return Optional.of('u'); - case 'ŭ': - return Optional.of('u'); - case 'Ů': - return Optional.of('u'); - case 'ů': - return Optional.of('u'); - case 'Ű': - return Optional.of('u'); - case 'ű': - return Optional.of('u'); - case 'Ų': - return Optional.of('u'); - case 'ų': - return Optional.of('u'); - case 'Ŵ': - return Optional.of('w'); - case 'ŵ': - return Optional.of('w'); - case 'Ŷ': - return Optional.of('y'); - case 'ŷ': - return Optional.of('y'); - case 'Ÿ': - return Optional.of('y'); - case 'Ź': - return Optional.of('z'); - case 'ź': - return Optional.of('z'); - case 'Ż': - return Optional.of('z'); - case 'ż': - return Optional.of('z'); - case 'Ž': - return Optional.of('z'); - case 'ž': - return Optional.of('z'); - case 'ſ': - return Optional.of('s'); - case 'ƀ': - return Optional.of('b'); - case 'Ɓ': - return Optional.of('b'); - case 'Ƃ': - return Optional.of('b'); - case 'ƃ': - return Optional.of('b'); - case 'Ɔ': - return Optional.of('o'); - case 'Ƈ': - return Optional.of('c'); - case 'ƈ': - return Optional.of('c'); - case 'Ɖ': - return Optional.of('d'); - case 'Ɗ': - return Optional.of('d'); - case 'Ƌ': - return Optional.of('d'); - case 'ƌ': - return Optional.of('d'); - case 'ƍ': - return Optional.of('d'); - case 'Ɛ': - return Optional.of('e'); - case 'Ƒ': - return Optional.of('f'); - case 'ƒ': - return Optional.of('f'); - case 'Ɠ': - return Optional.of('g'); - case 'Ɣ': - return Optional.of('g'); - case 'Ɩ': - return Optional.of('i'); - case 'Ɨ': - return Optional.of('i'); - case 'Ƙ': - return Optional.of('k'); - case 'ƙ': - return Optional.of('k'); - case 'ƚ': - return Optional.of('l'); - case 'ƛ': - return Optional.of('l'); - case 'Ɯ': - return Optional.of('w'); - case 'Ɲ': - return Optional.of('n'); - case 'ƞ': - return Optional.of('n'); - case 'Ɵ': - return Optional.of('o'); - case 'Ơ': - return Optional.of('o'); - case 'ơ': - return Optional.of('o'); - case 'Ƥ': - return Optional.of('p'); - case 'ƥ': - return Optional.of('p'); - case 'ƫ': - return Optional.of('t'); - case 'Ƭ': - return Optional.of('t'); - case 'ƭ': - return Optional.of('t'); - case 'Ʈ': - return Optional.of('t'); - case 'Ư': - return Optional.of('u'); - case 'ư': - return Optional.of('u'); - case 'Ʊ': - return Optional.of('y'); - case 'Ʋ': - return Optional.of('v'); - case 'Ƴ': - return Optional.of('y'); - case 'ƴ': - return Optional.of('y'); - case 'Ƶ': - return Optional.of('z'); - case 'ƶ': - return Optional.of('z'); - case 'ƿ': - return Optional.of('w'); - case 'Ǎ': - return Optional.of('a'); - case 'ǎ': - return Optional.of('a'); - case 'Ǐ': - return Optional.of('i'); - case 'ǐ': - return Optional.of('i'); - case 'Ǒ': - return Optional.of('o'); - case 'ǒ': - return Optional.of('o'); - case 'Ǔ': - return Optional.of('u'); - case 'ǔ': - return Optional.of('u'); - case 'Ǖ': - return Optional.of('u'); - case 'ǖ': - return Optional.of('u'); - case 'Ǘ': - return Optional.of('u'); - case 'ǘ': - return Optional.of('u'); - case 'Ǚ': - return Optional.of('u'); - case 'ǚ': - return Optional.of('u'); - case 'Ǜ': - return Optional.of('u'); - case 'ǜ': - return Optional.of('u'); - case 'Ǟ': - return Optional.of('a'); - case 'ǟ': - return Optional.of('a'); - case 'Ǡ': - return Optional.of('a'); - case 'ǡ': - return Optional.of('a'); - case 'Ǥ': - return Optional.of('g'); - case 'ǥ': - return Optional.of('g'); - case 'Ǧ': - return Optional.of('g'); - case 'ǧ': - return Optional.of('g'); - case 'Ǩ': - return Optional.of('k'); - case 'ǩ': - return Optional.of('k'); - case 'Ǫ': - return Optional.of('o'); - case 'ǫ': - return Optional.of('o'); - case 'Ǭ': - return Optional.of('o'); - case 'ǭ': - return Optional.of('o'); - case 'ǰ': - return Optional.of('j'); - case 'Dz': - return Optional.of('d'); - case 'Ǵ': - return Optional.of('g'); - case 'ǵ': - return Optional.of('g'); - case 'Ƿ': - return Optional.of('w'); - case 'Ǹ': - return Optional.of('n'); - case 'ǹ': - return Optional.of('n'); - case 'Ǻ': - return Optional.of('a'); - case 'ǻ': - return Optional.of('a'); - case 'Ǿ': - return Optional.of('o'); - case 'ǿ': - return Optional.of('o'); - case 'Ȁ': - return Optional.of('a'); - case 'ȁ': - return Optional.of('a'); - case 'Ȃ': - return Optional.of('a'); - case 'ȃ': - return Optional.of('a'); - case 'Ȅ': - return Optional.of('e'); - case 'ȅ': - return Optional.of('e'); - case 'Ȇ': - return Optional.of('e'); - case 'ȇ': - return Optional.of('e'); - case 'Ȉ': - return Optional.of('i'); - case 'ȉ': - return Optional.of('i'); - case 'Ȋ': - return Optional.of('i'); - case 'ȋ': - return Optional.of('i'); - case 'Ȍ': - return Optional.of('o'); - case 'ȍ': - return Optional.of('o'); - case 'Ȏ': - return Optional.of('o'); - case 'ȏ': - return Optional.of('o'); - case 'Ȑ': - return Optional.of('r'); - case 'ȑ': - return Optional.of('r'); - case 'Ȓ': - return Optional.of('r'); - case 'ȓ': - return Optional.of('r'); - case 'Ȕ': - return Optional.of('u'); - case 'ȕ': - return Optional.of('u'); - case 'Ȗ': - return Optional.of('u'); - case 'ȗ': - return Optional.of('u'); - case 'Ș': - return Optional.of('s'); - case 'ș': - return Optional.of('s'); - case 'Ț': - return Optional.of('t'); - case 'ț': - return Optional.of('t'); - case 'Ȝ': - return Optional.of('y'); - case 'ȝ': - return Optional.of('y'); - case 'Ȟ': - return Optional.of('h'); - case 'ȟ': - return Optional.of('h'); - case 'Ȥ': - return Optional.of('z'); - case 'ȥ': - return Optional.of('z'); - case 'Ȧ': - return Optional.of('a'); - case 'ȧ': - return Optional.of('a'); - case 'Ȩ': - return Optional.of('e'); - case 'ȩ': - return Optional.of('e'); - case 'Ȫ': - return Optional.of('o'); - case 'ȫ': - return Optional.of('o'); - case 'Ȭ': - return Optional.of('o'); - case 'ȭ': - return Optional.of('o'); - case 'Ȯ': - return Optional.of('o'); - case 'ȯ': - return Optional.of('o'); - case 'Ȱ': - return Optional.of('o'); - case 'ȱ': - return Optional.of('o'); - case 'Ȳ': - return Optional.of('y'); - case 'ȳ': - return Optional.of('y'); - case 'A': - return Optional.of('a'); - case 'B': - return Optional.of('b'); - case 'C': - return Optional.of('c'); - case 'D': - return Optional.of('d'); - case 'E': - return Optional.of('e'); - case 'F': - return Optional.of('f'); - case 'G': - return Optional.of('g'); - case 'H': - return Optional.of('h'); - case 'I': - return Optional.of('i'); - case 'J': - return Optional.of('j'); - case 'K': - return Optional.of('k'); - case 'L': - return Optional.of('l'); - case 'M': - return Optional.of('m'); - case 'N': - return Optional.of('n'); - case 'O': - return Optional.of('o'); - case 'P': - return Optional.of('p'); - case 'Q': - return Optional.of('q'); - case 'R': - return Optional.of('r'); - case 'S': - return Optional.of('s'); - case 'T': - return Optional.of('t'); - case 'U': - return Optional.of('u'); - case 'V': - return Optional.of('v'); - case 'W': - return Optional.of('w'); - case 'X': - return Optional.of('x'); - case 'Y': - return Optional.of('y'); - case 'Z': - return Optional.of('z'); - default: - return Optional.absent(); - } - } - - @Override - SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; - } -} diff --git a/java/com/android/dialer/smartdial/RussianSmartDialMap.java b/java/com/android/dialer/smartdial/RussianSmartDialMap.java deleted file mode 100644 index ada9182e1..000000000 --- a/java/com/android/dialer/smartdial/RussianSmartDialMap.java +++ /dev/null @@ -1,94 +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.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** A {@link SmartDialMap} for the Russian alphabet. */ -@SuppressWarnings("Guava") -final class RussianSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - // Reference: https://en.wikipedia.org/wiki/Russian_alphabet - static { - CHAR_TO_KEY_MAP.put('а', '2'); - CHAR_TO_KEY_MAP.put('б', '2'); - CHAR_TO_KEY_MAP.put('в', '2'); - CHAR_TO_KEY_MAP.put('г', '2'); - - CHAR_TO_KEY_MAP.put('д', '3'); - CHAR_TO_KEY_MAP.put('е', '3'); - CHAR_TO_KEY_MAP.put('ё', '3'); - CHAR_TO_KEY_MAP.put('ж', '3'); - CHAR_TO_KEY_MAP.put('з', '3'); - - CHAR_TO_KEY_MAP.put('и', '4'); - CHAR_TO_KEY_MAP.put('й', '4'); - CHAR_TO_KEY_MAP.put('к', '4'); - CHAR_TO_KEY_MAP.put('л', '4'); - - CHAR_TO_KEY_MAP.put('м', '5'); - CHAR_TO_KEY_MAP.put('н', '5'); - CHAR_TO_KEY_MAP.put('о', '5'); - CHAR_TO_KEY_MAP.put('п', '5'); - - CHAR_TO_KEY_MAP.put('р', '6'); - CHAR_TO_KEY_MAP.put('с', '6'); - CHAR_TO_KEY_MAP.put('т', '6'); - CHAR_TO_KEY_MAP.put('у', '6'); - - CHAR_TO_KEY_MAP.put('ф', '7'); - CHAR_TO_KEY_MAP.put('х', '7'); - CHAR_TO_KEY_MAP.put('ц', '7'); - CHAR_TO_KEY_MAP.put('ч', '7'); - - CHAR_TO_KEY_MAP.put('ш', '8'); - CHAR_TO_KEY_MAP.put('щ', '8'); - CHAR_TO_KEY_MAP.put('ъ', '8'); - CHAR_TO_KEY_MAP.put('ы', '8'); - - CHAR_TO_KEY_MAP.put('ь', '9'); - CHAR_TO_KEY_MAP.put('э', '9'); - CHAR_TO_KEY_MAP.put('ю', '9'); - CHAR_TO_KEY_MAP.put('я', '9'); - } - - private static RussianSmartDialMap instance; - - static RussianSmartDialMap getInstance() { - if (instance == null) { - instance = new RussianSmartDialMap(); - } - - return instance; - } - - private RussianSmartDialMap() {} - - @Override - Optional normalizeCharacter(char ch) { - ch = Character.toLowerCase(ch); - return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); - } - - @Override - SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; - } -} diff --git a/java/com/android/dialer/smartdial/SmartDialCursorLoader.java b/java/com/android/dialer/smartdial/SmartDialCursorLoader.java new file mode 100644 index 000000000..f6bc9325a --- /dev/null +++ b/java/com/android/dialer/smartdial/SmartDialCursorLoader.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2013 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.smartdial; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.Database; +import com.android.dialer.database.DialerDatabaseHelper; +import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; +import com.android.dialer.smartdial.util.SmartDialNameMatcher; +import com.android.dialer.util.PermissionsUtil; +import java.util.ArrayList; + +/** Implements a Loader class to asynchronously load SmartDial search results. */ +public class SmartDialCursorLoader extends AsyncTaskLoader { + + private static final String TAG = "SmartDialCursorLoader"; + private static final boolean DEBUG = false; + + private final Context mContext; + + private Cursor mCursor; + + private String mQuery; + private SmartDialNameMatcher mNameMatcher; + + private boolean mShowEmptyListForNullQuery = true; + + public SmartDialCursorLoader(Context context) { + super(context); + mContext = context; + } + + /** + * Configures the query string to be used to find SmartDial matches. + * + * @param query The query string user typed. + */ + public void configureQuery(String query) { + if (DEBUG) { + LogUtil.v(TAG, "Configure new query to be " + query); + } + mQuery = SmartDialNameMatcher.normalizeNumber(mContext, query); + + /** Constructs a name matcher object for matching names. */ + mNameMatcher = new SmartDialNameMatcher(mQuery); + mNameMatcher.setShouldMatchEmptyQuery(!mShowEmptyListForNullQuery); + } + + /** + * Queries the SmartDial database and loads results in background. + * + * @return Cursor of contacts that matches the SmartDial query. + */ + @Override + public Cursor loadInBackground() { + if (DEBUG) { + LogUtil.v(TAG, "Load in background " + mQuery); + } + + if (!PermissionsUtil.hasContactsReadPermissions(mContext)) { + return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); + } + + /** Loads results from the database helper. */ + final DialerDatabaseHelper dialerDatabaseHelper = + Database.get(mContext).getDatabaseHelper(mContext); + final ArrayList allMatches = + dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher); + + if (DEBUG) { + LogUtil.v(TAG, "Loaded matches " + allMatches.size()); + } + + /** Constructs a cursor for the returned array of results. */ + final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); + Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; + for (ContactNumber contact : allMatches) { + row[PhoneQuery.PHONE_ID] = contact.dataId; + row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; + row[PhoneQuery.CONTACT_ID] = contact.id; + row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; + row[PhoneQuery.PHOTO_ID] = contact.photoId; + row[PhoneQuery.DISPLAY_NAME] = contact.displayName; + row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence; + cursor.addRow(row); + } + return cursor; + } + + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + /** The Loader has been reset; ignore the result and invalidate the data. */ + releaseResources(cursor); + return; + } + + /** Hold a reference to the old data so it doesn't get garbage collected. */ + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + /** If the Loader is in a started state, deliver the results to the client. */ + super.deliverResult(cursor); + } + + /** Invalidate the old data as we don't need it any more. */ + if (oldCursor != null && oldCursor != cursor) { + releaseResources(oldCursor); + } + } + + @Override + protected void onStartLoading() { + if (mCursor != null) { + /** Deliver any previously loaded data immediately. */ + deliverResult(mCursor); + } + if (mCursor == null) { + /** Force loads every time as our results change with queries. */ + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ + cancelLoad(); + } + + @Override + protected void onReset() { + /** Ensure the loader has been stopped. */ + onStopLoading(); + + /** Release all previously saved query results. */ + if (mCursor != null) { + releaseResources(mCursor); + mCursor = null; + } + } + + @Override + public void onCanceled(Cursor cursor) { + super.onCanceled(cursor); + + /** The load has been canceled, so we should release the resources associated with 'data'. */ + releaseResources(cursor); + } + + private void releaseResources(Cursor cursor) { + if (cursor != null) { + cursor.close(); + } + } + + public void setShowEmptyListForNullQuery(boolean show) { + mShowEmptyListForNullQuery = show; + if (mNameMatcher != null) { + mNameMatcher.setShouldMatchEmptyQuery(!show); + } + } +} diff --git a/java/com/android/dialer/smartdial/SmartDialMap.java b/java/com/android/dialer/smartdial/SmartDialMap.java deleted file mode 100644 index bc5c9ea72..000000000 --- a/java/com/android/dialer/smartdial/SmartDialMap.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2016 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.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** Definition for utilities that supports smart dial in different languages. */ -@SuppressWarnings("Guava") -abstract class SmartDialMap { - - /** - * Returns true if the provided character can be mapped to a key on the dialpad. - * - *

      The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - */ - protected boolean isValidDialpadCharacter(char ch) { - return isValidDialpadAlphabeticChar(ch) || isValidDialpadNumericChar(ch); - } - - /** - * Returns true if the provided character is a letter and can be mapped to a key on the dialpad. - * - *

      The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - */ - protected boolean isValidDialpadAlphabeticChar(char ch) { - return getCharToKeyMap().containsKey(ch); - } - - /** - * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad. - */ - protected boolean isValidDialpadNumericChar(char ch) { - return '0' <= ch && ch <= '9'; - } - - /** - * Get the index of the key on the dialpad which the character corresponds to. - * - *

      The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - * - *

      An {@link Optional#absent()} is returned if the provided character can't be mapped to a key - * on the dialpad. - */ - protected Optional getDialpadIndex(char ch) { - if (isValidDialpadNumericChar(ch)) { - return Optional.of((byte) (ch - '0')); - } - - if (isValidDialpadAlphabeticChar(ch)) { - return Optional.of((byte) (getCharToKeyMap().get(ch) - '0')); - } - - return Optional.absent(); - } - - /** - * Get the actual numeric character on the dialpad which the character corresponds to. - * - *

      The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - * - *

      An {@link Optional#absent()} is returned if the provided character can't be mapped to a key - * on the dialpad. - */ - protected Optional getDialpadNumericCharacter(char ch) { - return isValidDialpadAlphabeticChar(ch) - ? Optional.of(getCharToKeyMap().get(ch)) - : Optional.absent(); - } - - /** - * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents - * from accented characters. - * - *

      An {@link Optional#absent()} is returned if the provided character can't be mapped to a key - * on the dialpad. - */ - abstract Optional normalizeCharacter(char ch); - - /** - * Returns a map in which each key is a normalized character and the corresponding value is a - * dialpad key. - */ - abstract SimpleArrayMap getCharToKeyMap(); -} diff --git a/java/com/android/dialer/smartdial/SmartDialMatchPosition.java b/java/com/android/dialer/smartdial/SmartDialMatchPosition.java deleted file mode 100644 index 8056ad723..000000000 --- a/java/com/android/dialer/smartdial/SmartDialMatchPosition.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2012 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.smartdial; - -import android.util.Log; -import java.util.ArrayList; - -/** - * Stores information about a range of characters matched in a display name The integers start and - * end indicate that the range start to end (exclusive) correspond to some characters in the query. - * Used to highlight certain parts of the contact's display name to indicate that those ranges - * matched the user's query. - */ -public class SmartDialMatchPosition { - - private static final String TAG = SmartDialMatchPosition.class.getSimpleName(); - - public int start; - public int end; - - public SmartDialMatchPosition(int start, int end) { - this.start = start; - this.end = end; - } - - /** - * Used by {@link SmartDialNameMatcher} to advance the positions of a match position found in a - * sub query. - * - * @param inList ArrayList of SmartDialMatchPositions to modify. - * @param toAdvance Offset to modify by. - */ - public static void advanceMatchPositions( - ArrayList inList, int toAdvance) { - for (int i = 0; i < inList.size(); i++) { - inList.get(i).advance(toAdvance); - } - } - - /** - * Used mainly for debug purposes. Displays contents of an ArrayList of SmartDialMatchPositions. - * - * @param list ArrayList of SmartDialMatchPositions to print out in a human readable fashion. - */ - public static void print(ArrayList list) { - for (int i = 0; i < list.size(); i++) { - SmartDialMatchPosition m = list.get(i); - Log.d(TAG, "[" + m.start + "," + m.end + "]"); - } - } - - private void advance(int toAdvance) { - this.start += toAdvance; - this.end += toAdvance; - } -} diff --git a/java/com/android/dialer/smartdial/SmartDialNameMatcher.java b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java deleted file mode 100644 index 4e3e0cc3f..000000000 --- a/java/com/android/dialer/smartdial/SmartDialNameMatcher.java +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Copyright (C) 2012 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.smartdial; - -import android.content.Context; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import com.android.dialer.smartdial.SmartDialPrefix.PhoneNumberTokens; -import java.util.ArrayList; - -/** - * {@link #SmartDialNameMatcher} contains utility functions to remove accents from accented - * characters and normalize a phone number. It also contains the matching logic that determines if a - * contact's display name matches a numeric query. The boolean variable {@link #ALLOW_INITIAL_MATCH} - * controls the behavior of the matching logic and determines whether we allow matches like 57 - - * (J)ohn (S)mith. - */ -public class SmartDialNameMatcher { - // Whether or not we allow matches like 57 - (J)ohn (S)mith - private static final boolean ALLOW_INITIAL_MATCH = true; - - // The maximum length of the initial we will match - typically set to 1 to minimize false - // positives - private static final int INITIAL_LENGTH_LIMIT = 1; - - private final ArrayList mMatchPositions = new ArrayList<>(); - private String mQuery; - - // Controls whether to treat an empty query as a match (with anything). - private boolean mShouldMatchEmptyQuery = false; - - public SmartDialNameMatcher(String query) { - mQuery = query; - } - - /** - * Strips a phone number of unnecessary characters (spaces, dashes, etc.) - * - * @param number Phone number we want to normalize - * @return Phone number consisting of digits from 0-9 - */ - public static String normalizeNumber(Context context, String number) { - return normalizeNumber(context, number, /* offset = */ 0); - } - - /** - * Strips a phone number of unnecessary characters (spaces, dashes, etc.) - * - * @param number Phone number we want to normalize - * @param offset Offset to start from - * @return Phone number consisting of digits from 0-9 - */ - public static String normalizeNumber(Context context, String number, int offset) { - final StringBuilder s = new StringBuilder(); - for (int i = offset; i < number.length(); i++) { - char ch = number.charAt(i); - if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) { - s.append(ch); - } - } - return s.toString(); - } - - /** - * Constructs empty highlight mask. Bit 0 at a position means there is no match, Bit 1 means there - * is a match and should be highlighted in the TextView. - * - * @param builder StringBuilder object - * @param length Length of the desired mask. - */ - private void constructEmptyMask(StringBuilder builder, int length) { - for (int i = 0; i < length; ++i) { - builder.append("0"); - } - } - - /** - * Replaces the 0-bit at a position with 1-bit, indicating that there is a match. - * - * @param builder StringBuilder object. - * @param matchPos Match Positions to mask as 1. - */ - private void replaceBitInMask(StringBuilder builder, SmartDialMatchPosition matchPos) { - for (int i = matchPos.start; i < matchPos.end; ++i) { - builder.replace(i, i + 1, "1"); - } - } - - /** - * Matches a phone number against a query. Let the test application overwrite the NANP setting. - * - * @param phoneNumber - Raw phone number - * @param query - Normalized query (only contains numbers from 0-9) - * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition - * with the matching positions otherwise - */ - @Nullable - public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber, String query) { - if (TextUtils.isEmpty(phoneNumber)) { - return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(0, 0) : null; - } - StringBuilder builder = new StringBuilder(); - constructEmptyMask(builder, phoneNumber.length()); - - // Try matching the number as is - SmartDialMatchPosition matchPos = - matchesNumberWithOffset(context, phoneNumber, query, /* offset = */ 0); - if (matchPos == null) { - PhoneNumberTokens phoneNumberTokens = SmartDialPrefix.parsePhoneNumber(context, phoneNumber); - - if (phoneNumberTokens.countryCodeOffset != 0) { - matchPos = - matchesNumberWithOffset( - context, phoneNumber, query, phoneNumberTokens.countryCodeOffset); - } - if (matchPos == null && phoneNumberTokens.nanpCodeOffset != 0) { - matchPos = - matchesNumberWithOffset(context, phoneNumber, query, phoneNumberTokens.nanpCodeOffset); - } - } - if (matchPos != null) { - replaceBitInMask(builder, matchPos); - } - return matchPos; - } - - /** - * Matches a phone number against the saved query, taking care of formatting characters and also - * taking into account country code prefixes and special NANP number treatment. - * - * @param phoneNumber - Raw phone number - * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition - * with the matching positions otherwise - */ - public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber) { - return matchesNumber(context, phoneNumber, mQuery); - } - - /** - * Matches a phone number against a query, taking care of formatting characters - * - * @param phoneNumber - Raw phone number - * @param query - Normalized query (only contains numbers from 0-9) - * @param offset - The position in the number to start the match against (used to ignore leading - * prefixes/country codes) - * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition - * with the matching positions otherwise - */ - private SmartDialMatchPosition matchesNumberWithOffset( - Context context, String phoneNumber, String query, int offset) { - if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(query)) { - return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(offset, offset) : null; - } - int queryAt = 0; - int numberAt = offset; - for (int i = offset; i < phoneNumber.length(); i++) { - if (queryAt == query.length()) { - break; - } - char ch = phoneNumber.charAt(i); - if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) { - if (ch != query.charAt(queryAt)) { - return null; - } - queryAt++; - } else { - if (queryAt == 0) { - // Found a separator before any part of the query was matched, so advance the - // offset to avoid prematurely highlighting separators before the rest of the - // query. - // E.g. don't highlight the first '-' if we're matching 1-510-111-1111 with - // '510'. - // However, if the current offset is 0, just include the beginning separators - // anyway, otherwise the highlighting ends up looking weird. - // E.g. if we're matching (510)-111-1111 with '510', we should include the - // first '('. - if (offset != 0) { - offset++; - } - } - } - numberAt++; - } - return new SmartDialMatchPosition(0 + offset, numberAt); - } - - /** - * This function iterates through each token in the display name, trying to match the query to the - * numeric equivalent of the token. - * - *

      A token is defined as a range in the display name delimited by characters that have no latin - * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters - * - '王'). Transliteration from non-latin characters to latin character will be done on a best - * effort basis - e.g. 'Ü' - 'u'. - * - *

      For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips", - * "thomas", and "jr". - * - *

      A match must begin at the start of a token. For example, typing 846(Tho) would match - * "Phillips Thomas", but 466(hom) would not. - * - *

      Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred - * S)mith. - * - * @param displayName The normalized(no accented characters) display name we intend to match - * against. - * @param query The string of digits that we want to match the display name to. - * @param matchList An array list of {@link SmartDialMatchPosition}s that we add matched positions - * to. - * @return Returns true if a combination of the tokens in displayName match the query string - * contained in query. If the function returns true, matchList will contain an ArrayList of - * match positions (multiple matches correspond to initial matches). - */ - private boolean matchesCombination( - Context context, - String displayName, - String query, - ArrayList matchList) { - StringBuilder builder = new StringBuilder(); - constructEmptyMask(builder, displayName.length()); - final int nameLength = displayName.length(); - final int queryLength = query.length(); - - if (nameLength < queryLength) { - return false; - } - - if (queryLength == 0) { - return false; - } - - // The current character index in displayName - // E.g. 3 corresponds to 'd' in "Fred Smith" - int nameStart = 0; - - // The current character in the query we are trying to match the displayName against - int queryStart = 0; - - // The start position of the current token we are inspecting - int tokenStart = 0; - - // The number of non-alphabetic characters we've encountered so far in the current match. - // E.g. if we've currently matched 3733764849 to (Fred Smith W)illiam, then the - // seperatorCount should be 2. This allows us to correctly calculate offsets for the match - // positions - int seperatorCount = 0; - - ArrayList partial = new ArrayList(); - // Keep going until we reach the end of displayName - while (nameStart < nameLength && queryStart < queryLength) { - char ch = displayName.charAt(nameStart); - // Strip diacritics from accented characters if any - ch = CompositeSmartDialMap.normalizeCharacter(context, ch); - if (CompositeSmartDialMap.isValidDialpadCharacter(context, ch)) { - if (CompositeSmartDialMap.isValidDialpadAlphabeticChar(context, ch)) { - ch = CompositeSmartDialMap.getDialpadNumericCharacter(context, ch); - } - if (ch != query.charAt(queryStart)) { - // Failed to match the current character in the query. - - // Case 1: Failed to match the first character in the query. Skip to the next - // token since there is no chance of this token matching the query. - - // Case 2: Previous characters in the query matched, but the current character - // failed to match. This happened in the middle of a token. Skip to the next - // token since there is no chance of this token matching the query. - - // Case 3: Previous characters in the query matched, but the current character - // failed to match. This happened right at the start of the current token. In - // this case, we should restart the query and try again with the current token. - // Otherwise, we would fail to match a query like "964"(yog) against a name - // Yo-Yoghurt because the query match would fail on the 3rd character, and - // then skip to the end of the "Yoghurt" token. - - if (queryStart == 0 - || CompositeSmartDialMap.isValidDialpadCharacter( - context, - CompositeSmartDialMap.normalizeCharacter( - context, displayName.charAt(nameStart - 1)))) { - // skip to the next token, in the case of 1 or 2. - while (nameStart < nameLength - && CompositeSmartDialMap.isValidDialpadCharacter( - context, - CompositeSmartDialMap.normalizeCharacter( - context, displayName.charAt(nameStart)))) { - nameStart++; - } - nameStart++; - } - - // Restart the query and set the correct token position - queryStart = 0; - seperatorCount = 0; - tokenStart = nameStart; - } else { - if (queryStart == queryLength - 1) { - - // As much as possible, we prioritize a full token match over a sub token - // one so if we find a full token match, we can return right away - matchList.add( - new SmartDialMatchPosition(tokenStart, queryLength + tokenStart + seperatorCount)); - for (SmartDialMatchPosition match : matchList) { - replaceBitInMask(builder, match); - } - return true; - } else if (ALLOW_INITIAL_MATCH && queryStart < INITIAL_LENGTH_LIMIT) { - // we matched the first character. - // branch off and see if we can find another match with the remaining - // characters in the query string and the remaining tokens - // find the next separator in the query string - int j; - for (j = nameStart; j < nameLength; j++) { - if (!CompositeSmartDialMap.isValidDialpadCharacter( - context, - CompositeSmartDialMap.normalizeCharacter(context, displayName.charAt(j)))) { - break; - } - } - // this means there is at least one character left after the separator - if (j < nameLength - 1) { - final String remainder = displayName.substring(j + 1); - final ArrayList partialTemp = new ArrayList<>(); - if (matchesCombination( - context, remainder, query.substring(queryStart + 1), partialTemp)) { - - // store the list of possible match positions - SmartDialMatchPosition.advanceMatchPositions(partialTemp, j + 1); - partialTemp.add(0, new SmartDialMatchPosition(nameStart, nameStart + 1)); - // we found a partial token match, store the data in a - // temp buffer and return it if we end up not finding a full - // token match - partial = partialTemp; - } - } - } - nameStart++; - queryStart++; - // we matched the current character in the name against one in the query, - // continue and see if the rest of the characters match - } - } else { - // found a separator, we skip this character and continue to the next one - nameStart++; - if (queryStart == 0) { - // This means we found a separator before the start of a token, - // so we should increment the token's start position to reflect its true - // start position - tokenStart = nameStart; - } else { - // Otherwise this separator was found in the middle of a token being matched, - // so increase the separator count - seperatorCount++; - } - } - } - // if we have no complete match at this point, then we attempt to fall back to the partial - // token match(if any). If we don't allow initial matching (ALLOW_INITIAL_MATCH = false) - // then partial will always be empty. - if (!partial.isEmpty()) { - matchList.addAll(partial); - for (SmartDialMatchPosition match : matchList) { - replaceBitInMask(builder, match); - } - return true; - } - return false; - } - - /** - * This function iterates through each token in the display name, trying to match the query to the - * numeric equivalent of the token. - * - *

      A token is defined as a range in the display name delimited by characters that have no latin - * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters - * - '王'). Transliteration from non-latin characters to latin character will be done on a best - * effort basis - e.g. 'Ü' - 'u'. - * - *

      For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips", - * "thomas", and "jr". - * - *

      A match must begin at the start of a token. For example, typing 846(Tho) would match - * "Phillips Thomas", but 466(hom) would not. - * - *

      Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred - * S)mith. - * - * @param displayName The normalized(no accented characters) display name we intend to match - * against. - * @return Returns true if a combination of the tokens in displayName match the query string - * contained in query. If the function returns true, matchList will contain an ArrayList of - * match positions (multiple matches correspond to initial matches). - */ - public boolean matches(Context context, String displayName) { - mMatchPositions.clear(); - return matchesCombination(context, displayName, mQuery, mMatchPositions); - } - - public ArrayList getMatchPositions() { - // Return a clone of mMatchPositions so that the caller can use it without - // worrying about it changing - return new ArrayList<>(mMatchPositions); - } - - public String getQuery() { - return mQuery; - } - - public void setQuery(String query) { - mQuery = query; - } - - public void setShouldMatchEmptyQuery(boolean matches) { - mShouldMatchEmptyQuery = matches; - } -} diff --git a/java/com/android/dialer/smartdial/SmartDialPrefix.java b/java/com/android/dialer/smartdial/SmartDialPrefix.java deleted file mode 100644 index b9c1f8c11..000000000 --- a/java/com/android/dialer/smartdial/SmartDialPrefix.java +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2013 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.smartdial; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.support.annotation.VisibleForTesting; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; - -/** - * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported - * prefix combinations for contact names, and also methods to find supported prefix combinations for - * contacts' phone numbers. Each contact name is separated into several tokens, such as first name, - * middle name, family name etc. Each phone number is also separated into country code, NANP area - * code, and local number if such separation is possible. - */ -public class SmartDialPrefix { - - /** - * The number of starting and ending tokens in a contact's name considered for initials. For - * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed - * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced - * by their initials in contact name matching. Users can look up this contact by combinations of - * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such - * as "CF" "DF" "ACF" "ADF" etc. - */ - private static final int LAST_TOKENS_FOR_INITIALS = 2; - - private static final int FIRST_TOKENS_FOR_INITIALS = 2; - - /** The country code of the user's sim card obtained by calling getSimCountryIso */ - private static final String PREF_USER_SIM_COUNTRY_CODE = - "DialtactsActivity_user_sim_country_code"; - - private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null; - - private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT; - /** Indicates whether user is in NANP regions. */ - private static boolean sUserInNanpRegion = false; - /** Set of country names that use NANP code. */ - private static Set sNanpCountries = null; - /** Set of supported country codes in front of the phone number. */ - private static Set sCountryCodes = null; - - private static boolean sNanpInitialized = false; - - /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */ - public static void initializeNanpSettings(Context context) { - final TelephonyManager manager = - (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - if (manager != null) { - sUserSimCountryCode = manager.getSimCountryIso(); - } - - final SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - - if (sUserSimCountryCode != null) { - /** Updates shared preferences with the latest country obtained from getSimCountryIso. */ - prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply(); - } else { - /** Uses previously stored country code if loading fails. */ - sUserSimCountryCode = - prefs.getString(PREF_USER_SIM_COUNTRY_CODE, PREF_USER_SIM_COUNTRY_CODE_DEFAULT); - } - /** Queries the NANP country list to find out whether user is in a NANP region. */ - sUserInNanpRegion = isCountryNanp(sUserSimCountryCode); - sNanpInitialized = true; - } - - /** - * Parses a contact's name into a list of separated tokens. - * - * @param contactName Contact's name stored in string. - * @return A list of name tokens, for example separated first names, last name, etc. - */ - public static ArrayList parseToIndexTokens(Context context, String contactName) { - final int length = contactName.length(); - final ArrayList result = new ArrayList<>(); - char c; - final StringBuilder currentIndexToken = new StringBuilder(); - /** - * Iterates through the whole name string. If the current character is a valid character, append - * it to the current token. If the current character is not a valid character, for example space - * " ", mark the current token as complete and add it to the list of tokens. - */ - for (int i = 0; i < length; i++) { - c = CompositeSmartDialMap.normalizeCharacter(context, contactName.charAt(i)); - if (CompositeSmartDialMap.isValidDialpadCharacter(context, c)) { - /** Converts a character into the number on dialpad that represents the character. */ - currentIndexToken.append(CompositeSmartDialMap.getDialpadIndex(context, c)); - } else { - if (currentIndexToken.length() != 0) { - result.add(currentIndexToken.toString()); - } - currentIndexToken.delete(0, currentIndexToken.length()); - } - } - - /** Adds the last token in case it has not been added. */ - if (currentIndexToken.length() != 0) { - result.add(currentIndexToken.toString()); - } - return result; - } - - /** - * Generates a list of strings that any prefix of any string in the list can be used to look up - * the contact's name. - * - * @param index The contact's name in string. - * @return A List of strings, whose prefix can be used to look up the contact. - */ - public static ArrayList generateNamePrefixes(Context context, String index) { - final ArrayList result = new ArrayList<>(); - - /** Parses the name into a list of tokens. */ - final ArrayList indexTokens = parseToIndexTokens(context, index); - - if (indexTokens.size() > 0) { - /** - * Adds the full token combinations to the list. For example, a contact with name "Albert Ben - * Ed Foster" can be looked up by any prefix of the following strings "Foster" "EdFoster" - * "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of look up that contains only - * one token, and that spans multiple continuous tokens. - */ - final StringBuilder fullNameToken = new StringBuilder(); - for (int i = indexTokens.size() - 1; i >= 0; i--) { - fullNameToken.insert(0, indexTokens.get(i)); - result.add(fullNameToken.toString()); - } - - /** - * Adds initial combinations to the list, with the number of initials restricted by {@link - * #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}. For example, a contact - * with name "Albert Ben Ed Foster" can be looked up by any prefix of the following strings - * "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster" "AEFoster" and "ABEFoster". This covers - * all cases of initial lookup. - */ - ArrayList fullNames = new ArrayList<>(); - fullNames.add(indexTokens.get(indexTokens.size() - 1)); - final int recursiveNameStart = result.size(); - int recursiveNameEnd = result.size(); - String initial = ""; - for (int i = indexTokens.size() - 2; i >= 0; i--) { - if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS) - || (i < FIRST_TOKENS_FOR_INITIALS)) { - initial = indexTokens.get(i).substring(0, 1); - - /** Recursively adds initial combinations to the list. */ - for (int j = 0; j < fullNames.size(); ++j) { - result.add(initial + fullNames.get(j)); - } - for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) { - result.add(initial + result.get(j)); - } - recursiveNameEnd = result.size(); - final String currentFullName = fullNames.get(fullNames.size() - 1); - fullNames.add(indexTokens.get(i) + currentFullName); - } - } - } - - return result; - } - - /** - * Computes a list of number strings based on tokens of a given phone number. Any prefix of any - * string in the list can be used to look up the phone number. The list include the full phone - * number, the national number if there is a country code in the phone number, and the local - * number if there is an area code in the phone number following the NANP format. For example, if - * a user has phone number +41 71 394 8392, the list will contain 41713948392 and 713948392. Any - * prefix to either of the strings can be used to look up the phone number. If a user has a phone - * number +1 555-302-3029 (NANP format), the list will contain 15553023029, 5553023029, and - * 3023029. - * - * @param number String of user's phone number. - * @return A list of strings where any prefix of any entry can be used to look up the number. - */ - public static ArrayList parseToNumberTokens(Context context, String number) { - final ArrayList result = new ArrayList<>(); - if (!TextUtils.isEmpty(number)) { - /** Adds the full number to the list. */ - result.add(SmartDialNameMatcher.normalizeNumber(context, number)); - - final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(context, number); - if (phoneNumberTokens == null) { - return result; - } - - if (phoneNumberTokens.countryCodeOffset != 0) { - result.add( - SmartDialNameMatcher.normalizeNumber( - context, number, phoneNumberTokens.countryCodeOffset)); - } - - if (phoneNumberTokens.nanpCodeOffset != 0) { - result.add( - SmartDialNameMatcher.normalizeNumber( - context, number, phoneNumberTokens.nanpCodeOffset)); - } - } - return result; - } - - /** - * Parses a phone number to find out whether it has country code and NANP area code. - * - * @param number Raw phone number. - * @return a PhoneNumberToken instance with country code, NANP code information. - */ - public static PhoneNumberTokens parsePhoneNumber(Context context, String number) { - String countryCode = ""; - int countryCodeOffset = 0; - int nanpNumberOffset = 0; - - if (!TextUtils.isEmpty(number)) { - String normalizedNumber = SmartDialNameMatcher.normalizeNumber(context, number); - if (number.charAt(0) == '+') { - /** If the number starts with '+', tries to find valid country code. */ - for (int i = 1; i <= 1 + 3; i++) { - if (number.length() <= i) { - break; - } - countryCode = number.substring(1, i); - if (isValidCountryCode(countryCode)) { - countryCodeOffset = i; - break; - } - } - } else { - /** - * If the number does not start with '+', finds out whether it is in NANP format and has '1' - * preceding the number. - */ - if ((normalizedNumber.length() == 11) - && (normalizedNumber.charAt(0) == '1') - && (sUserInNanpRegion)) { - countryCode = "1"; - countryCodeOffset = number.indexOf(normalizedNumber.charAt(1)); - if (countryCodeOffset == -1) { - countryCodeOffset = 0; - } - } - } - - /** If user is in NANP region, finds out whether a number is in NANP format. */ - if (sUserInNanpRegion) { - String areaCode = ""; - if (countryCode.equals("") && normalizedNumber.length() == 10) { - /** - * if the number has no country code but fits the NANP format, extracts the NANP area - * code, and finds out offset of the local number. - */ - areaCode = normalizedNumber.substring(0, 3); - } else if (countryCode.equals("1") && normalizedNumber.length() == 11) { - /** - * If the number has country code '1', finds out area code and offset of the local number. - */ - areaCode = normalizedNumber.substring(1, 4); - } - if (!areaCode.equals("")) { - final int areaCodeIndex = number.indexOf(areaCode); - if (areaCodeIndex != -1) { - nanpNumberOffset = number.indexOf(areaCode) + 3; - } - } - } - } - return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset); - } - - /** Checkes whether a country code is valid. */ - private static boolean isValidCountryCode(String countryCode) { - if (sCountryCodes == null) { - sCountryCodes = initCountryCodes(); - } - return sCountryCodes.contains(countryCode); - } - - private static Set initCountryCodes() { - final HashSet result = new HashSet(); - result.add("1"); - result.add("7"); - result.add("20"); - result.add("27"); - result.add("30"); - result.add("31"); - result.add("32"); - result.add("33"); - result.add("34"); - result.add("36"); - result.add("39"); - result.add("40"); - result.add("41"); - result.add("43"); - result.add("44"); - result.add("45"); - result.add("46"); - result.add("47"); - result.add("48"); - result.add("49"); - result.add("51"); - result.add("52"); - result.add("53"); - result.add("54"); - result.add("55"); - result.add("56"); - result.add("57"); - result.add("58"); - result.add("60"); - result.add("61"); - result.add("62"); - result.add("63"); - result.add("64"); - result.add("65"); - result.add("66"); - result.add("81"); - result.add("82"); - result.add("84"); - result.add("86"); - result.add("90"); - result.add("91"); - result.add("92"); - result.add("93"); - result.add("94"); - result.add("95"); - result.add("98"); - result.add("211"); - result.add("212"); - result.add("213"); - result.add("216"); - result.add("218"); - result.add("220"); - result.add("221"); - result.add("222"); - result.add("223"); - result.add("224"); - result.add("225"); - result.add("226"); - result.add("227"); - result.add("228"); - result.add("229"); - result.add("230"); - result.add("231"); - result.add("232"); - result.add("233"); - result.add("234"); - result.add("235"); - result.add("236"); - result.add("237"); - result.add("238"); - result.add("239"); - result.add("240"); - result.add("241"); - result.add("242"); - result.add("243"); - result.add("244"); - result.add("245"); - result.add("246"); - result.add("247"); - result.add("248"); - result.add("249"); - result.add("250"); - result.add("251"); - result.add("252"); - result.add("253"); - result.add("254"); - result.add("255"); - result.add("256"); - result.add("257"); - result.add("258"); - result.add("260"); - result.add("261"); - result.add("262"); - result.add("263"); - result.add("264"); - result.add("265"); - result.add("266"); - result.add("267"); - result.add("268"); - result.add("269"); - result.add("290"); - result.add("291"); - result.add("297"); - result.add("298"); - result.add("299"); - result.add("350"); - result.add("351"); - result.add("352"); - result.add("353"); - result.add("354"); - result.add("355"); - result.add("356"); - result.add("357"); - result.add("358"); - result.add("359"); - result.add("370"); - result.add("371"); - result.add("372"); - result.add("373"); - result.add("374"); - result.add("375"); - result.add("376"); - result.add("377"); - result.add("378"); - result.add("379"); - result.add("380"); - result.add("381"); - result.add("382"); - result.add("385"); - result.add("386"); - result.add("387"); - result.add("389"); - result.add("420"); - result.add("421"); - result.add("423"); - result.add("500"); - result.add("501"); - result.add("502"); - result.add("503"); - result.add("504"); - result.add("505"); - result.add("506"); - result.add("507"); - result.add("508"); - result.add("509"); - result.add("590"); - result.add("591"); - result.add("592"); - result.add("593"); - result.add("594"); - result.add("595"); - result.add("596"); - result.add("597"); - result.add("598"); - result.add("599"); - result.add("670"); - result.add("672"); - result.add("673"); - result.add("674"); - result.add("675"); - result.add("676"); - result.add("677"); - result.add("678"); - result.add("679"); - result.add("680"); - result.add("681"); - result.add("682"); - result.add("683"); - result.add("685"); - result.add("686"); - result.add("687"); - result.add("688"); - result.add("689"); - result.add("690"); - result.add("691"); - result.add("692"); - result.add("800"); - result.add("808"); - result.add("850"); - result.add("852"); - result.add("853"); - result.add("855"); - result.add("856"); - result.add("870"); - result.add("878"); - result.add("880"); - result.add("881"); - result.add("882"); - result.add("883"); - result.add("886"); - result.add("888"); - result.add("960"); - result.add("961"); - result.add("962"); - result.add("963"); - result.add("964"); - result.add("965"); - result.add("966"); - result.add("967"); - result.add("968"); - result.add("970"); - result.add("971"); - result.add("972"); - result.add("973"); - result.add("974"); - result.add("975"); - result.add("976"); - result.add("977"); - result.add("979"); - result.add("992"); - result.add("993"); - result.add("994"); - result.add("995"); - result.add("996"); - result.add("998"); - return result; - } - - /** - * Indicates whether the given country uses NANP numbers - * - * @param country ISO 3166 country code (case doesn't matter) - * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise - * @see - * https://en.wikipedia.org/wiki/North_American_Numbering_Plan - */ - @VisibleForTesting - public static boolean isCountryNanp(String country) { - if (TextUtils.isEmpty(country)) { - return false; - } - if (sNanpCountries == null) { - sNanpCountries = initNanpCountries(); - } - return sNanpCountries.contains(country.toUpperCase()); - } - - private static Set initNanpCountries() { - final HashSet result = new HashSet(); - result.add("US"); // United States - result.add("CA"); // Canada - result.add("AS"); // American Samoa - result.add("AI"); // Anguilla - result.add("AG"); // Antigua and Barbuda - result.add("BS"); // Bahamas - result.add("BB"); // Barbados - result.add("BM"); // Bermuda - result.add("VG"); // British Virgin Islands - result.add("KY"); // Cayman Islands - result.add("DM"); // Dominica - result.add("DO"); // Dominican Republic - result.add("GD"); // Grenada - result.add("GU"); // Guam - result.add("JM"); // Jamaica - result.add("PR"); // Puerto Rico - result.add("MS"); // Montserrat - result.add("MP"); // Northern Mariana Islands - result.add("KN"); // Saint Kitts and Nevis - result.add("LC"); // Saint Lucia - result.add("VC"); // Saint Vincent and the Grenadines - result.add("TT"); // Trinidad and Tobago - result.add("TC"); // Turks and Caicos Islands - result.add("VI"); // U.S. Virgin Islands - return result; - } - - /** - * Returns whether the user is in a region that uses Nanp format based on the sim location. - * - * @return Whether user is in Nanp region. - */ - public static boolean getUserInNanpRegion() { - return sUserInNanpRegion; - } - - /** Explicitly setting the user Nanp to the given boolean */ - @VisibleForTesting - public static void setUserInNanpRegion(boolean userInNanpRegion) { - sUserInNanpRegion = userInNanpRegion; - } - - /** Class to record phone number parsing information. */ - public static class PhoneNumberTokens { - - /** Country code of the phone number. */ - final String countryCode; - - /** Offset of national number after the country code. */ - final int countryCodeOffset; - - /** Offset of local number after NANP area code. */ - final int nanpCodeOffset; - - public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) { - this.countryCode = countryCode; - this.countryCodeOffset = countryCodeOffset; - this.nanpCodeOffset = nanpCodeOffset; - } - } -} diff --git a/java/com/android/dialer/smartdial/UkrainianSmartDialMap.java b/java/com/android/dialer/smartdial/UkrainianSmartDialMap.java deleted file mode 100644 index 8ba53c45f..000000000 --- a/java/com/android/dialer/smartdial/UkrainianSmartDialMap.java +++ /dev/null @@ -1,93 +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.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** A {@link SmartDialMap} for the Ukrainian alphabet. */ -final class UkrainianSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - // Reference: https://en.wikipedia.org/wiki/Ukrainian_alphabet - static { - CHAR_TO_KEY_MAP.put('а', '2'); - CHAR_TO_KEY_MAP.put('б', '2'); - CHAR_TO_KEY_MAP.put('в', '2'); - CHAR_TO_KEY_MAP.put('г', '2'); - CHAR_TO_KEY_MAP.put('ґ', '2'); - - CHAR_TO_KEY_MAP.put('д', '3'); - CHAR_TO_KEY_MAP.put('е', '3'); - CHAR_TO_KEY_MAP.put('є', '3'); - CHAR_TO_KEY_MAP.put('ж', '3'); - CHAR_TO_KEY_MAP.put('з', '3'); - - CHAR_TO_KEY_MAP.put('и', '4'); - CHAR_TO_KEY_MAP.put('і', '4'); - CHAR_TO_KEY_MAP.put('ї', '4'); - CHAR_TO_KEY_MAP.put('й', '4'); - CHAR_TO_KEY_MAP.put('к', '4'); - CHAR_TO_KEY_MAP.put('л', '4'); - - CHAR_TO_KEY_MAP.put('м', '5'); - CHAR_TO_KEY_MAP.put('н', '5'); - CHAR_TO_KEY_MAP.put('о', '5'); - CHAR_TO_KEY_MAP.put('п', '5'); - - CHAR_TO_KEY_MAP.put('р', '6'); - CHAR_TO_KEY_MAP.put('с', '6'); - CHAR_TO_KEY_MAP.put('т', '6'); - CHAR_TO_KEY_MAP.put('у', '6'); - - CHAR_TO_KEY_MAP.put('ф', '7'); - CHAR_TO_KEY_MAP.put('х', '7'); - CHAR_TO_KEY_MAP.put('ц', '7'); - CHAR_TO_KEY_MAP.put('ч', '7'); - - CHAR_TO_KEY_MAP.put('ш', '8'); - CHAR_TO_KEY_MAP.put('щ', '8'); - - CHAR_TO_KEY_MAP.put('ь', '9'); - CHAR_TO_KEY_MAP.put('ю', '9'); - CHAR_TO_KEY_MAP.put('я', '9'); - } - - private static UkrainianSmartDialMap instance; - - static UkrainianSmartDialMap getInstance() { - if (instance == null) { - instance = new UkrainianSmartDialMap(); - } - - return instance; - } - - private UkrainianSmartDialMap() {} - - @Override - Optional normalizeCharacter(char ch) { - ch = Character.toLowerCase(ch); - return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); - } - - @Override - SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; - } -} diff --git a/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java b/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java new file mode 100644 index 000000000..cbe6afa97 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java @@ -0,0 +1,91 @@ +/* + * 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.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Bulgarian alphabet. */ +@SuppressWarnings("Guava") +final class BulgarianSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + // Reference: https://en.wikipedia.org/wiki/Bulgarian_alphabet + static { + CHAR_TO_KEY_MAP.put('а', '2'); + CHAR_TO_KEY_MAP.put('б', '2'); + CHAR_TO_KEY_MAP.put('в', '2'); + CHAR_TO_KEY_MAP.put('г', '2'); + + CHAR_TO_KEY_MAP.put('д', '3'); + CHAR_TO_KEY_MAP.put('е', '3'); + CHAR_TO_KEY_MAP.put('ж', '3'); + CHAR_TO_KEY_MAP.put('з', '3'); + + CHAR_TO_KEY_MAP.put('и', '4'); + CHAR_TO_KEY_MAP.put('й', '4'); + CHAR_TO_KEY_MAP.put('к', '4'); + CHAR_TO_KEY_MAP.put('л', '4'); + + CHAR_TO_KEY_MAP.put('м', '5'); + CHAR_TO_KEY_MAP.put('н', '5'); + CHAR_TO_KEY_MAP.put('о', '5'); + + CHAR_TO_KEY_MAP.put('п', '6'); + CHAR_TO_KEY_MAP.put('р', '6'); + CHAR_TO_KEY_MAP.put('с', '6'); + + CHAR_TO_KEY_MAP.put('т', '7'); + CHAR_TO_KEY_MAP.put('у', '7'); + CHAR_TO_KEY_MAP.put('ф', '7'); + CHAR_TO_KEY_MAP.put('х', '7'); + + CHAR_TO_KEY_MAP.put('ц', '8'); + CHAR_TO_KEY_MAP.put('ч', '8'); + CHAR_TO_KEY_MAP.put('ш', '8'); + CHAR_TO_KEY_MAP.put('щ', '8'); + + CHAR_TO_KEY_MAP.put('ъ', '9'); + CHAR_TO_KEY_MAP.put('ь', '9'); + CHAR_TO_KEY_MAP.put('ю', '9'); + CHAR_TO_KEY_MAP.put('я', '9'); + } + + private static BulgarianSmartDialMap instance; + + static BulgarianSmartDialMap getInstance() { + if (instance == null) { + instance = new BulgarianSmartDialMap(); + } + + return instance; + } + + private BulgarianSmartDialMap() {} + + @Override + Optional normalizeCharacter(char ch) { + ch = Character.toLowerCase(ch); + return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} diff --git a/java/com/android/dialer/smartdial/map/CompositeSmartDialMap.java b/java/com/android/dialer/smartdial/map/CompositeSmartDialMap.java new file mode 100644 index 000000000..df32d4ce7 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/CompositeSmartDialMap.java @@ -0,0 +1,172 @@ +/* + * 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.smartdial.map; + +import android.content.Context; +import android.support.annotation.VisibleForTesting; +import android.support.v4.util.SimpleArrayMap; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.configprovider.ConfigProviderBindings; +import com.google.common.base.Optional; + +/** + * A utility class that combines the functionality of two implementations of {@link SmartDialMap} so + * that we support smart dial for dual alphabets. + * + *

      Of the two implementations of {@link SmartDialMap}, the default one always takes precedence. + * The second one is consulted only when the default one is unable to provide a valid result. + * + *

      Note that the second implementation can be absent if it is not defined for the system's 1st + * language preference. + */ +@SuppressWarnings("Guava") +public class CompositeSmartDialMap { + @VisibleForTesting + public static final String FLAG_ENABLE_DUAL_ALPHABETS = "enable_dual_alphabets_on_t9"; + + private static final SmartDialMap DEFAULT_MAP = LatinSmartDialMap.getInstance(); + + // A map in which each key is an ISO 639-2 language code and the corresponding value is a + // SmartDialMap + private static final SimpleArrayMap EXTRA_MAPS = new SimpleArrayMap<>(); + + static { + EXTRA_MAPS.put("bul", BulgarianSmartDialMap.getInstance()); + EXTRA_MAPS.put("rus", RussianSmartDialMap.getInstance()); + EXTRA_MAPS.put("ukr", UkrainianSmartDialMap.getInstance()); + } + + private CompositeSmartDialMap() {} + + /** + * Returns true if the provided character can be mapped to a key on the dialpad. + * + *

      The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + */ + public static boolean isValidDialpadCharacter(Context context, char ch) { + if (DEFAULT_MAP.isValidDialpadCharacter(ch)) { + return true; + } + + Optional extraMap = getExtraMap(context); + return extraMap.isPresent() && extraMap.get().isValidDialpadCharacter(ch); + } + + /** + * Returns true if the provided character is a letter, and can be mapped to a key on the dialpad. + * + *

      The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + */ + public static boolean isValidDialpadAlphabeticChar(Context context, char ch) { + if (DEFAULT_MAP.isValidDialpadAlphabeticChar(ch)) { + return true; + } + + Optional extraMap = getExtraMap(context); + return extraMap.isPresent() && extraMap.get().isValidDialpadAlphabeticChar(ch); + } + + /** + * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad. + */ + public static boolean isValidDialpadNumericChar(Context context, char ch) { + if (DEFAULT_MAP.isValidDialpadNumericChar(ch)) { + return true; + } + + Optional extraMap = getExtraMap(context); + return extraMap.isPresent() && extraMap.get().isValidDialpadNumericChar(ch); + } + + /** + * Get the index of the key on the dialpad which the character corresponds to. + * + *

      The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + * + *

      If the provided character can't be mapped to a key on the dialpad, return -1. + */ + public static byte getDialpadIndex(Context context, char ch) { + Optional dialpadIndex = DEFAULT_MAP.getDialpadIndex(ch); + if (dialpadIndex.isPresent()) { + return dialpadIndex.get(); + } + + Optional extraMap = getExtraMap(context); + if (extraMap.isPresent()) { + dialpadIndex = extraMap.get().getDialpadIndex(ch); + } + + return dialpadIndex.isPresent() ? dialpadIndex.get() : -1; + } + + /** + * Get the actual numeric character on the dialpad which the character corresponds to. + * + *

      The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + * + *

      If the provided character can't be mapped to a key on the dialpad, return the character. + */ + public static char getDialpadNumericCharacter(Context context, char ch) { + Optional dialpadNumericChar = DEFAULT_MAP.getDialpadNumericCharacter(ch); + if (dialpadNumericChar.isPresent()) { + return dialpadNumericChar.get(); + } + + Optional extraMap = getExtraMap(context); + if (extraMap.isPresent()) { + dialpadNumericChar = extraMap.get().getDialpadNumericCharacter(ch); + } + + return dialpadNumericChar.isPresent() ? dialpadNumericChar.get() : ch; + } + + /** + * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents + * from accented characters. + * + *

      If the provided character can't be mapped to a key on the dialpad, return the character. + */ + public static char normalizeCharacter(Context context, char ch) { + Optional normalizedChar = DEFAULT_MAP.normalizeCharacter(ch); + if (normalizedChar.isPresent()) { + return normalizedChar.get(); + } + + Optional extraMap = getExtraMap(context); + if (extraMap.isPresent()) { + normalizedChar = extraMap.get().normalizeCharacter(ch); + } + + return normalizedChar.isPresent() ? normalizedChar.get() : ch; + } + + @VisibleForTesting + static Optional getExtraMap(Context context) { + if (!ConfigProviderBindings.get(context).getBoolean(FLAG_ENABLE_DUAL_ALPHABETS, false)) { + return Optional.absent(); + } + + String languageCode = CompatUtils.getLocale(context).getISO3Language(); + return EXTRA_MAPS.containsKey(languageCode) + ? Optional.of(EXTRA_MAPS.get(languageCode)) + : Optional.absent(); + } +} diff --git a/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java new file mode 100644 index 000000000..052af05c0 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java @@ -0,0 +1,785 @@ +/* + * Copyright (C) 2016 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.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Latin alphabet, which is for T9 dialpad searching. */ +@SuppressWarnings("Guava") +final class LatinSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + static { + CHAR_TO_KEY_MAP.put('a', '2'); + CHAR_TO_KEY_MAP.put('b', '2'); + CHAR_TO_KEY_MAP.put('c', '2'); + + CHAR_TO_KEY_MAP.put('d', '3'); + CHAR_TO_KEY_MAP.put('e', '3'); + CHAR_TO_KEY_MAP.put('f', '3'); + + CHAR_TO_KEY_MAP.put('g', '4'); + CHAR_TO_KEY_MAP.put('h', '4'); + CHAR_TO_KEY_MAP.put('i', '4'); + + CHAR_TO_KEY_MAP.put('j', '5'); + CHAR_TO_KEY_MAP.put('k', '5'); + CHAR_TO_KEY_MAP.put('l', '5'); + + CHAR_TO_KEY_MAP.put('m', '6'); + CHAR_TO_KEY_MAP.put('n', '6'); + CHAR_TO_KEY_MAP.put('o', '6'); + + CHAR_TO_KEY_MAP.put('p', '7'); + CHAR_TO_KEY_MAP.put('q', '7'); + CHAR_TO_KEY_MAP.put('r', '7'); + CHAR_TO_KEY_MAP.put('s', '7'); + + CHAR_TO_KEY_MAP.put('t', '8'); + CHAR_TO_KEY_MAP.put('u', '8'); + CHAR_TO_KEY_MAP.put('v', '8'); + + CHAR_TO_KEY_MAP.put('w', '9'); + CHAR_TO_KEY_MAP.put('x', '9'); + CHAR_TO_KEY_MAP.put('y', '9'); + CHAR_TO_KEY_MAP.put('z', '9'); + } + + private static LatinSmartDialMap instance; + + static LatinSmartDialMap getInstance() { + if (instance == null) { + instance = new LatinSmartDialMap(); + } + + return instance; + } + + private LatinSmartDialMap() {} + + /* + * The switch statement in this function was generated using the python code: + * from unidecode import unidecode + * for i in range(192, 564): + * char = unichr(i) + * decoded = unidecode(char) + * # Unicode characters that decompose into multiple characters i.e. + * # into ss are not supported for now + * if (len(decoded) == 1 and decoded.isalpha()): + * print "case '" + char + "': return Optional.of('" + unidecode(char) + "');" + * + * This gives us a way to map characters containing accents/diacritics to their + * alphabetic equivalents. The unidecode library can be found at: + * http://pypi.python.org/pypi/Unidecode/0.04.1 + * + * Also remaps all upper case latin characters to their lower case equivalents. + */ + @Override + Optional normalizeCharacter(char ch) { + if (isValidDialpadAlphabeticChar(ch)) { + return Optional.of(ch); + } + + switch (ch) { + case 'À': + return Optional.of('a'); + case 'Á': + return Optional.of('a'); + case 'Â': + return Optional.of('a'); + case 'Ã': + return Optional.of('a'); + case 'Ä': + return Optional.of('a'); + case 'Å': + return Optional.of('a'); + case 'Ç': + return Optional.of('c'); + case 'È': + return Optional.of('e'); + case 'É': + return Optional.of('e'); + case 'Ê': + return Optional.of('e'); + case 'Ë': + return Optional.of('e'); + case 'Ì': + return Optional.of('i'); + case 'Í': + return Optional.of('i'); + case 'Î': + return Optional.of('i'); + case 'Ï': + return Optional.of('i'); + case 'Ð': + return Optional.of('d'); + case 'Ñ': + return Optional.of('n'); + case 'Ò': + return Optional.of('o'); + case 'Ó': + return Optional.of('o'); + case 'Ô': + return Optional.of('o'); + case 'Õ': + return Optional.of('o'); + case 'Ö': + return Optional.of('o'); + case '×': + return Optional.of('x'); + case 'Ø': + return Optional.of('o'); + case 'Ù': + return Optional.of('u'); + case 'Ú': + return Optional.of('u'); + case 'Û': + return Optional.of('u'); + case 'Ü': + return Optional.of('u'); + case 'Ý': + return Optional.of('u'); + case 'à': + return Optional.of('a'); + case 'á': + return Optional.of('a'); + case 'â': + return Optional.of('a'); + case 'ã': + return Optional.of('a'); + case 'ä': + return Optional.of('a'); + case 'å': + return Optional.of('a'); + case 'ç': + return Optional.of('c'); + case 'è': + return Optional.of('e'); + case 'é': + return Optional.of('e'); + case 'ê': + return Optional.of('e'); + case 'ë': + return Optional.of('e'); + case 'ì': + return Optional.of('i'); + case 'í': + return Optional.of('i'); + case 'î': + return Optional.of('i'); + case 'ï': + return Optional.of('i'); + case 'ð': + return Optional.of('d'); + case 'ñ': + return Optional.of('n'); + case 'ò': + return Optional.of('o'); + case 'ó': + return Optional.of('o'); + case 'ô': + return Optional.of('o'); + case 'õ': + return Optional.of('o'); + case 'ö': + return Optional.of('o'); + case 'ø': + return Optional.of('o'); + case 'ù': + return Optional.of('u'); + case 'ú': + return Optional.of('u'); + case 'û': + return Optional.of('u'); + case 'ü': + return Optional.of('u'); + case 'ý': + return Optional.of('y'); + case 'ÿ': + return Optional.of('y'); + case 'Ā': + return Optional.of('a'); + case 'ā': + return Optional.of('a'); + case 'Ă': + return Optional.of('a'); + case 'ă': + return Optional.of('a'); + case 'Ą': + return Optional.of('a'); + case 'ą': + return Optional.of('a'); + case 'Ć': + return Optional.of('c'); + case 'ć': + return Optional.of('c'); + case 'Ĉ': + return Optional.of('c'); + case 'ĉ': + return Optional.of('c'); + case 'Ċ': + return Optional.of('c'); + case 'ċ': + return Optional.of('c'); + case 'Č': + return Optional.of('c'); + case 'č': + return Optional.of('c'); + case 'Ď': + return Optional.of('d'); + case 'ď': + return Optional.of('d'); + case 'Đ': + return Optional.of('d'); + case 'đ': + return Optional.of('d'); + case 'Ē': + return Optional.of('e'); + case 'ē': + return Optional.of('e'); + case 'Ĕ': + return Optional.of('e'); + case 'ĕ': + return Optional.of('e'); + case 'Ė': + return Optional.of('e'); + case 'ė': + return Optional.of('e'); + case 'Ę': + return Optional.of('e'); + case 'ę': + return Optional.of('e'); + case 'Ě': + return Optional.of('e'); + case 'ě': + return Optional.of('e'); + case 'Ĝ': + return Optional.of('g'); + case 'ĝ': + return Optional.of('g'); + case 'Ğ': + return Optional.of('g'); + case 'ğ': + return Optional.of('g'); + case 'Ġ': + return Optional.of('g'); + case 'ġ': + return Optional.of('g'); + case 'Ģ': + return Optional.of('g'); + case 'ģ': + return Optional.of('g'); + case 'Ĥ': + return Optional.of('h'); + case 'ĥ': + return Optional.of('h'); + case 'Ħ': + return Optional.of('h'); + case 'ħ': + return Optional.of('h'); + case 'Ĩ': + return Optional.of('i'); + case 'ĩ': + return Optional.of('i'); + case 'Ī': + return Optional.of('i'); + case 'ī': + return Optional.of('i'); + case 'Ĭ': + return Optional.of('i'); + case 'ĭ': + return Optional.of('i'); + case 'Į': + return Optional.of('i'); + case 'į': + return Optional.of('i'); + case 'İ': + return Optional.of('i'); + case 'ı': + return Optional.of('i'); + case 'Ĵ': + return Optional.of('j'); + case 'ĵ': + return Optional.of('j'); + case 'Ķ': + return Optional.of('k'); + case 'ķ': + return Optional.of('k'); + case 'ĸ': + return Optional.of('k'); + case 'Ĺ': + return Optional.of('l'); + case 'ĺ': + return Optional.of('l'); + case 'Ļ': + return Optional.of('l'); + case 'ļ': + return Optional.of('l'); + case 'Ľ': + return Optional.of('l'); + case 'ľ': + return Optional.of('l'); + case 'Ŀ': + return Optional.of('l'); + case 'ŀ': + return Optional.of('l'); + case 'Ł': + return Optional.of('l'); + case 'ł': + return Optional.of('l'); + case 'Ń': + return Optional.of('n'); + case 'ń': + return Optional.of('n'); + case 'Ņ': + return Optional.of('n'); + case 'ņ': + return Optional.of('n'); + case 'Ň': + return Optional.of('n'); + case 'ň': + return Optional.of('n'); + case 'Ō': + return Optional.of('o'); + case 'ō': + return Optional.of('o'); + case 'Ŏ': + return Optional.of('o'); + case 'ŏ': + return Optional.of('o'); + case 'Ő': + return Optional.of('o'); + case 'ő': + return Optional.of('o'); + case 'Ŕ': + return Optional.of('r'); + case 'ŕ': + return Optional.of('r'); + case 'Ŗ': + return Optional.of('r'); + case 'ŗ': + return Optional.of('r'); + case 'Ř': + return Optional.of('r'); + case 'ř': + return Optional.of('r'); + case 'Ś': + return Optional.of('s'); + case 'ś': + return Optional.of('s'); + case 'Ŝ': + return Optional.of('s'); + case 'ŝ': + return Optional.of('s'); + case 'Ş': + return Optional.of('s'); + case 'ş': + return Optional.of('s'); + case 'Š': + return Optional.of('s'); + case 'š': + return Optional.of('s'); + case 'Ţ': + return Optional.of('t'); + case 'ţ': + return Optional.of('t'); + case 'Ť': + return Optional.of('t'); + case 'ť': + return Optional.of('t'); + case 'Ŧ': + return Optional.of('t'); + case 'ŧ': + return Optional.of('t'); + case 'Ũ': + return Optional.of('u'); + case 'ũ': + return Optional.of('u'); + case 'Ū': + return Optional.of('u'); + case 'ū': + return Optional.of('u'); + case 'Ŭ': + return Optional.of('u'); + case 'ŭ': + return Optional.of('u'); + case 'Ů': + return Optional.of('u'); + case 'ů': + return Optional.of('u'); + case 'Ű': + return Optional.of('u'); + case 'ű': + return Optional.of('u'); + case 'Ų': + return Optional.of('u'); + case 'ų': + return Optional.of('u'); + case 'Ŵ': + return Optional.of('w'); + case 'ŵ': + return Optional.of('w'); + case 'Ŷ': + return Optional.of('y'); + case 'ŷ': + return Optional.of('y'); + case 'Ÿ': + return Optional.of('y'); + case 'Ź': + return Optional.of('z'); + case 'ź': + return Optional.of('z'); + case 'Ż': + return Optional.of('z'); + case 'ż': + return Optional.of('z'); + case 'Ž': + return Optional.of('z'); + case 'ž': + return Optional.of('z'); + case 'ſ': + return Optional.of('s'); + case 'ƀ': + return Optional.of('b'); + case 'Ɓ': + return Optional.of('b'); + case 'Ƃ': + return Optional.of('b'); + case 'ƃ': + return Optional.of('b'); + case 'Ɔ': + return Optional.of('o'); + case 'Ƈ': + return Optional.of('c'); + case 'ƈ': + return Optional.of('c'); + case 'Ɖ': + return Optional.of('d'); + case 'Ɗ': + return Optional.of('d'); + case 'Ƌ': + return Optional.of('d'); + case 'ƌ': + return Optional.of('d'); + case 'ƍ': + return Optional.of('d'); + case 'Ɛ': + return Optional.of('e'); + case 'Ƒ': + return Optional.of('f'); + case 'ƒ': + return Optional.of('f'); + case 'Ɠ': + return Optional.of('g'); + case 'Ɣ': + return Optional.of('g'); + case 'Ɩ': + return Optional.of('i'); + case 'Ɨ': + return Optional.of('i'); + case 'Ƙ': + return Optional.of('k'); + case 'ƙ': + return Optional.of('k'); + case 'ƚ': + return Optional.of('l'); + case 'ƛ': + return Optional.of('l'); + case 'Ɯ': + return Optional.of('w'); + case 'Ɲ': + return Optional.of('n'); + case 'ƞ': + return Optional.of('n'); + case 'Ɵ': + return Optional.of('o'); + case 'Ơ': + return Optional.of('o'); + case 'ơ': + return Optional.of('o'); + case 'Ƥ': + return Optional.of('p'); + case 'ƥ': + return Optional.of('p'); + case 'ƫ': + return Optional.of('t'); + case 'Ƭ': + return Optional.of('t'); + case 'ƭ': + return Optional.of('t'); + case 'Ʈ': + return Optional.of('t'); + case 'Ư': + return Optional.of('u'); + case 'ư': + return Optional.of('u'); + case 'Ʊ': + return Optional.of('y'); + case 'Ʋ': + return Optional.of('v'); + case 'Ƴ': + return Optional.of('y'); + case 'ƴ': + return Optional.of('y'); + case 'Ƶ': + return Optional.of('z'); + case 'ƶ': + return Optional.of('z'); + case 'ƿ': + return Optional.of('w'); + case 'Ǎ': + return Optional.of('a'); + case 'ǎ': + return Optional.of('a'); + case 'Ǐ': + return Optional.of('i'); + case 'ǐ': + return Optional.of('i'); + case 'Ǒ': + return Optional.of('o'); + case 'ǒ': + return Optional.of('o'); + case 'Ǔ': + return Optional.of('u'); + case 'ǔ': + return Optional.of('u'); + case 'Ǖ': + return Optional.of('u'); + case 'ǖ': + return Optional.of('u'); + case 'Ǘ': + return Optional.of('u'); + case 'ǘ': + return Optional.of('u'); + case 'Ǚ': + return Optional.of('u'); + case 'ǚ': + return Optional.of('u'); + case 'Ǜ': + return Optional.of('u'); + case 'ǜ': + return Optional.of('u'); + case 'Ǟ': + return Optional.of('a'); + case 'ǟ': + return Optional.of('a'); + case 'Ǡ': + return Optional.of('a'); + case 'ǡ': + return Optional.of('a'); + case 'Ǥ': + return Optional.of('g'); + case 'ǥ': + return Optional.of('g'); + case 'Ǧ': + return Optional.of('g'); + case 'ǧ': + return Optional.of('g'); + case 'Ǩ': + return Optional.of('k'); + case 'ǩ': + return Optional.of('k'); + case 'Ǫ': + return Optional.of('o'); + case 'ǫ': + return Optional.of('o'); + case 'Ǭ': + return Optional.of('o'); + case 'ǭ': + return Optional.of('o'); + case 'ǰ': + return Optional.of('j'); + case 'Dz': + return Optional.of('d'); + case 'Ǵ': + return Optional.of('g'); + case 'ǵ': + return Optional.of('g'); + case 'Ƿ': + return Optional.of('w'); + case 'Ǹ': + return Optional.of('n'); + case 'ǹ': + return Optional.of('n'); + case 'Ǻ': + return Optional.of('a'); + case 'ǻ': + return Optional.of('a'); + case 'Ǿ': + return Optional.of('o'); + case 'ǿ': + return Optional.of('o'); + case 'Ȁ': + return Optional.of('a'); + case 'ȁ': + return Optional.of('a'); + case 'Ȃ': + return Optional.of('a'); + case 'ȃ': + return Optional.of('a'); + case 'Ȅ': + return Optional.of('e'); + case 'ȅ': + return Optional.of('e'); + case 'Ȇ': + return Optional.of('e'); + case 'ȇ': + return Optional.of('e'); + case 'Ȉ': + return Optional.of('i'); + case 'ȉ': + return Optional.of('i'); + case 'Ȋ': + return Optional.of('i'); + case 'ȋ': + return Optional.of('i'); + case 'Ȍ': + return Optional.of('o'); + case 'ȍ': + return Optional.of('o'); + case 'Ȏ': + return Optional.of('o'); + case 'ȏ': + return Optional.of('o'); + case 'Ȑ': + return Optional.of('r'); + case 'ȑ': + return Optional.of('r'); + case 'Ȓ': + return Optional.of('r'); + case 'ȓ': + return Optional.of('r'); + case 'Ȕ': + return Optional.of('u'); + case 'ȕ': + return Optional.of('u'); + case 'Ȗ': + return Optional.of('u'); + case 'ȗ': + return Optional.of('u'); + case 'Ș': + return Optional.of('s'); + case 'ș': + return Optional.of('s'); + case 'Ț': + return Optional.of('t'); + case 'ț': + return Optional.of('t'); + case 'Ȝ': + return Optional.of('y'); + case 'ȝ': + return Optional.of('y'); + case 'Ȟ': + return Optional.of('h'); + case 'ȟ': + return Optional.of('h'); + case 'Ȥ': + return Optional.of('z'); + case 'ȥ': + return Optional.of('z'); + case 'Ȧ': + return Optional.of('a'); + case 'ȧ': + return Optional.of('a'); + case 'Ȩ': + return Optional.of('e'); + case 'ȩ': + return Optional.of('e'); + case 'Ȫ': + return Optional.of('o'); + case 'ȫ': + return Optional.of('o'); + case 'Ȭ': + return Optional.of('o'); + case 'ȭ': + return Optional.of('o'); + case 'Ȯ': + return Optional.of('o'); + case 'ȯ': + return Optional.of('o'); + case 'Ȱ': + return Optional.of('o'); + case 'ȱ': + return Optional.of('o'); + case 'Ȳ': + return Optional.of('y'); + case 'ȳ': + return Optional.of('y'); + case 'A': + return Optional.of('a'); + case 'B': + return Optional.of('b'); + case 'C': + return Optional.of('c'); + case 'D': + return Optional.of('d'); + case 'E': + return Optional.of('e'); + case 'F': + return Optional.of('f'); + case 'G': + return Optional.of('g'); + case 'H': + return Optional.of('h'); + case 'I': + return Optional.of('i'); + case 'J': + return Optional.of('j'); + case 'K': + return Optional.of('k'); + case 'L': + return Optional.of('l'); + case 'M': + return Optional.of('m'); + case 'N': + return Optional.of('n'); + case 'O': + return Optional.of('o'); + case 'P': + return Optional.of('p'); + case 'Q': + return Optional.of('q'); + case 'R': + return Optional.of('r'); + case 'S': + return Optional.of('s'); + case 'T': + return Optional.of('t'); + case 'U': + return Optional.of('u'); + case 'V': + return Optional.of('v'); + case 'W': + return Optional.of('w'); + case 'X': + return Optional.of('x'); + case 'Y': + return Optional.of('y'); + case 'Z': + return Optional.of('z'); + default: + return Optional.absent(); + } + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} diff --git a/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java b/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java new file mode 100644 index 000000000..5038520c2 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java @@ -0,0 +1,94 @@ +/* + * 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.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Russian alphabet. */ +@SuppressWarnings("Guava") +final class RussianSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + // Reference: https://en.wikipedia.org/wiki/Russian_alphabet + static { + CHAR_TO_KEY_MAP.put('а', '2'); + CHAR_TO_KEY_MAP.put('б', '2'); + CHAR_TO_KEY_MAP.put('в', '2'); + CHAR_TO_KEY_MAP.put('г', '2'); + + CHAR_TO_KEY_MAP.put('д', '3'); + CHAR_TO_KEY_MAP.put('е', '3'); + CHAR_TO_KEY_MAP.put('ё', '3'); + CHAR_TO_KEY_MAP.put('ж', '3'); + CHAR_TO_KEY_MAP.put('з', '3'); + + CHAR_TO_KEY_MAP.put('и', '4'); + CHAR_TO_KEY_MAP.put('й', '4'); + CHAR_TO_KEY_MAP.put('к', '4'); + CHAR_TO_KEY_MAP.put('л', '4'); + + CHAR_TO_KEY_MAP.put('м', '5'); + CHAR_TO_KEY_MAP.put('н', '5'); + CHAR_TO_KEY_MAP.put('о', '5'); + CHAR_TO_KEY_MAP.put('п', '5'); + + CHAR_TO_KEY_MAP.put('р', '6'); + CHAR_TO_KEY_MAP.put('с', '6'); + CHAR_TO_KEY_MAP.put('т', '6'); + CHAR_TO_KEY_MAP.put('у', '6'); + + CHAR_TO_KEY_MAP.put('ф', '7'); + CHAR_TO_KEY_MAP.put('х', '7'); + CHAR_TO_KEY_MAP.put('ц', '7'); + CHAR_TO_KEY_MAP.put('ч', '7'); + + CHAR_TO_KEY_MAP.put('ш', '8'); + CHAR_TO_KEY_MAP.put('щ', '8'); + CHAR_TO_KEY_MAP.put('ъ', '8'); + CHAR_TO_KEY_MAP.put('ы', '8'); + + CHAR_TO_KEY_MAP.put('ь', '9'); + CHAR_TO_KEY_MAP.put('э', '9'); + CHAR_TO_KEY_MAP.put('ю', '9'); + CHAR_TO_KEY_MAP.put('я', '9'); + } + + private static RussianSmartDialMap instance; + + static RussianSmartDialMap getInstance() { + if (instance == null) { + instance = new RussianSmartDialMap(); + } + + return instance; + } + + private RussianSmartDialMap() {} + + @Override + Optional normalizeCharacter(char ch) { + ch = Character.toLowerCase(ch); + return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} diff --git a/java/com/android/dialer/smartdial/map/SmartDialMap.java b/java/com/android/dialer/smartdial/map/SmartDialMap.java new file mode 100644 index 000000000..c74dd2893 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/SmartDialMap.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2016 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.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** Definition for utilities that supports smart dial in different languages. */ +@SuppressWarnings("Guava") +abstract class SmartDialMap { + + /** + * Returns true if the provided character can be mapped to a key on the dialpad. + * + *

      The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + */ + protected boolean isValidDialpadCharacter(char ch) { + return isValidDialpadAlphabeticChar(ch) || isValidDialpadNumericChar(ch); + } + + /** + * Returns true if the provided character is a letter and can be mapped to a key on the dialpad. + * + *

      The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + */ + protected boolean isValidDialpadAlphabeticChar(char ch) { + return getCharToKeyMap().containsKey(ch); + } + + /** + * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad. + */ + protected boolean isValidDialpadNumericChar(char ch) { + return '0' <= ch && ch <= '9'; + } + + /** + * Get the index of the key on the dialpad which the character corresponds to. + * + *

      The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + * + *

      An {@link Optional#absent()} is returned if the provided character can't be mapped to a key + * on the dialpad. + */ + protected Optional getDialpadIndex(char ch) { + if (isValidDialpadNumericChar(ch)) { + return Optional.of((byte) (ch - '0')); + } + + if (isValidDialpadAlphabeticChar(ch)) { + return Optional.of((byte) (getCharToKeyMap().get(ch) - '0')); + } + + return Optional.absent(); + } + + /** + * Get the actual numeric character on the dialpad which the character corresponds to. + * + *

      The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + * + *

      An {@link Optional#absent()} is returned if the provided character can't be mapped to a key + * on the dialpad. + */ + protected Optional getDialpadNumericCharacter(char ch) { + return isValidDialpadAlphabeticChar(ch) + ? Optional.of(getCharToKeyMap().get(ch)) + : Optional.absent(); + } + + /** + * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents + * from accented characters. + * + *

      An {@link Optional#absent()} is returned if the provided character can't be mapped to a key + * on the dialpad. + */ + abstract Optional normalizeCharacter(char ch); + + /** + * Returns a map in which each key is a normalized character and the corresponding value is a + * dialpad key. + */ + abstract SimpleArrayMap getCharToKeyMap(); +} diff --git a/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java b/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java new file mode 100644 index 000000000..28dbc0d61 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java @@ -0,0 +1,93 @@ +/* + * 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.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Ukrainian alphabet. */ +final class UkrainianSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + // Reference: https://en.wikipedia.org/wiki/Ukrainian_alphabet + static { + CHAR_TO_KEY_MAP.put('а', '2'); + CHAR_TO_KEY_MAP.put('б', '2'); + CHAR_TO_KEY_MAP.put('в', '2'); + CHAR_TO_KEY_MAP.put('г', '2'); + CHAR_TO_KEY_MAP.put('ґ', '2'); + + CHAR_TO_KEY_MAP.put('д', '3'); + CHAR_TO_KEY_MAP.put('е', '3'); + CHAR_TO_KEY_MAP.put('є', '3'); + CHAR_TO_KEY_MAP.put('ж', '3'); + CHAR_TO_KEY_MAP.put('з', '3'); + + CHAR_TO_KEY_MAP.put('и', '4'); + CHAR_TO_KEY_MAP.put('і', '4'); + CHAR_TO_KEY_MAP.put('ї', '4'); + CHAR_TO_KEY_MAP.put('й', '4'); + CHAR_TO_KEY_MAP.put('к', '4'); + CHAR_TO_KEY_MAP.put('л', '4'); + + CHAR_TO_KEY_MAP.put('м', '5'); + CHAR_TO_KEY_MAP.put('н', '5'); + CHAR_TO_KEY_MAP.put('о', '5'); + CHAR_TO_KEY_MAP.put('п', '5'); + + CHAR_TO_KEY_MAP.put('р', '6'); + CHAR_TO_KEY_MAP.put('с', '6'); + CHAR_TO_KEY_MAP.put('т', '6'); + CHAR_TO_KEY_MAP.put('у', '6'); + + CHAR_TO_KEY_MAP.put('ф', '7'); + CHAR_TO_KEY_MAP.put('х', '7'); + CHAR_TO_KEY_MAP.put('ц', '7'); + CHAR_TO_KEY_MAP.put('ч', '7'); + + CHAR_TO_KEY_MAP.put('ш', '8'); + CHAR_TO_KEY_MAP.put('щ', '8'); + + CHAR_TO_KEY_MAP.put('ь', '9'); + CHAR_TO_KEY_MAP.put('ю', '9'); + CHAR_TO_KEY_MAP.put('я', '9'); + } + + private static UkrainianSmartDialMap instance; + + static UkrainianSmartDialMap getInstance() { + if (instance == null) { + instance = new UkrainianSmartDialMap(); + } + + return instance; + } + + private UkrainianSmartDialMap() {} + + @Override + Optional normalizeCharacter(char ch) { + ch = Character.toLowerCase(ch); + return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} diff --git a/java/com/android/dialer/smartdial/util/SmartDialMatchPosition.java b/java/com/android/dialer/smartdial/util/SmartDialMatchPosition.java new file mode 100644 index 000000000..db317ae6b --- /dev/null +++ b/java/com/android/dialer/smartdial/util/SmartDialMatchPosition.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012 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.smartdial.util; + +import com.android.dialer.common.LogUtil; +import java.util.ArrayList; + +/** + * Stores information about a range of characters matched in a display name The integers start and + * end indicate that the range start to end (exclusive) correspond to some characters in the query. + * Used to highlight certain parts of the contact's display name to indicate that those ranges + * matched the user's query. + */ +public class SmartDialMatchPosition { + + private static final String TAG = SmartDialMatchPosition.class.getSimpleName(); + + public int start; + public int end; + + public SmartDialMatchPosition(int start, int end) { + this.start = start; + this.end = end; + } + + /** + * Used by {@link SmartDialNameMatcher} to advance the positions of a match position found in a + * sub query. + * + * @param inList ArrayList of SmartDialMatchPositions to modify. + * @param toAdvance Offset to modify by. + */ + public static void advanceMatchPositions( + ArrayList inList, int toAdvance) { + for (int i = 0; i < inList.size(); i++) { + inList.get(i).advance(toAdvance); + } + } + + /** + * Used mainly for debug purposes. Displays contents of an ArrayList of SmartDialMatchPositions. + * + * @param list ArrayList of SmartDialMatchPositions to print out in a human readable fashion. + */ + public static void print(ArrayList list) { + for (int i = 0; i < list.size(); i++) { + SmartDialMatchPosition m = list.get(i); + LogUtil.d(TAG, "[" + m.start + "," + m.end + "]"); + } + } + + private void advance(int toAdvance) { + this.start += toAdvance; + this.end += toAdvance; + } +} diff --git a/java/com/android/dialer/smartdial/util/SmartDialNameMatcher.java b/java/com/android/dialer/smartdial/util/SmartDialNameMatcher.java new file mode 100644 index 000000000..725c88c57 --- /dev/null +++ b/java/com/android/dialer/smartdial/util/SmartDialNameMatcher.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2012 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.smartdial.util; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import com.android.dialer.smartdial.map.CompositeSmartDialMap; +import com.android.dialer.smartdial.util.SmartDialPrefix.PhoneNumberTokens; +import java.util.ArrayList; + +/** + * {@link #SmartDialNameMatcher} contains utility functions to remove accents from accented + * characters and normalize a phone number. It also contains the matching logic that determines if a + * contact's display name matches a numeric query. The boolean variable {@link #ALLOW_INITIAL_MATCH} + * controls the behavior of the matching logic and determines whether we allow matches like 57 - + * (J)ohn (S)mith. + */ +public class SmartDialNameMatcher { + // Whether or not we allow matches like 57 - (J)ohn (S)mith + private static final boolean ALLOW_INITIAL_MATCH = true; + + // The maximum length of the initial we will match - typically set to 1 to minimize false + // positives + private static final int INITIAL_LENGTH_LIMIT = 1; + + private final ArrayList mMatchPositions = new ArrayList<>(); + private String mQuery; + + // Controls whether to treat an empty query as a match (with anything). + private boolean mShouldMatchEmptyQuery = false; + + public SmartDialNameMatcher(String query) { + mQuery = query; + } + + /** + * Strips a phone number of unnecessary characters (spaces, dashes, etc.) + * + * @param number Phone number we want to normalize + * @return Phone number consisting of digits from 0-9 + */ + public static String normalizeNumber(Context context, String number) { + return normalizeNumber(context, number, /* offset = */ 0); + } + + /** + * Strips a phone number of unnecessary characters (spaces, dashes, etc.) + * + * @param number Phone number we want to normalize + * @param offset Offset to start from + * @return Phone number consisting of digits from 0-9 + */ + public static String normalizeNumber(Context context, String number, int offset) { + final StringBuilder s = new StringBuilder(); + for (int i = offset; i < number.length(); i++) { + char ch = number.charAt(i); + if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) { + s.append(ch); + } + } + return s.toString(); + } + + /** + * Constructs empty highlight mask. Bit 0 at a position means there is no match, Bit 1 means there + * is a match and should be highlighted in the TextView. + * + * @param builder StringBuilder object + * @param length Length of the desired mask. + */ + private void constructEmptyMask(StringBuilder builder, int length) { + for (int i = 0; i < length; ++i) { + builder.append("0"); + } + } + + /** + * Replaces the 0-bit at a position with 1-bit, indicating that there is a match. + * + * @param builder StringBuilder object. + * @param matchPos Match Positions to mask as 1. + */ + private void replaceBitInMask(StringBuilder builder, SmartDialMatchPosition matchPos) { + for (int i = matchPos.start; i < matchPos.end; ++i) { + builder.replace(i, i + 1, "1"); + } + } + + /** + * Matches a phone number against a query. Let the test application overwrite the NANP setting. + * + * @param phoneNumber - Raw phone number + * @param query - Normalized query (only contains numbers from 0-9) + * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition + * with the matching positions otherwise + */ + @Nullable + public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber, String query) { + if (TextUtils.isEmpty(phoneNumber)) { + return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(0, 0) : null; + } + StringBuilder builder = new StringBuilder(); + constructEmptyMask(builder, phoneNumber.length()); + + // Try matching the number as is + SmartDialMatchPosition matchPos = + matchesNumberWithOffset(context, phoneNumber, query, /* offset = */ 0); + if (matchPos == null) { + PhoneNumberTokens phoneNumberTokens = SmartDialPrefix.parsePhoneNumber(context, phoneNumber); + + if (phoneNumberTokens.countryCodeOffset != 0) { + matchPos = + matchesNumberWithOffset( + context, phoneNumber, query, phoneNumberTokens.countryCodeOffset); + } + if (matchPos == null && phoneNumberTokens.nanpCodeOffset != 0) { + matchPos = + matchesNumberWithOffset(context, phoneNumber, query, phoneNumberTokens.nanpCodeOffset); + } + } + if (matchPos != null) { + replaceBitInMask(builder, matchPos); + } + return matchPos; + } + + /** + * Matches a phone number against the saved query, taking care of formatting characters and also + * taking into account country code prefixes and special NANP number treatment. + * + * @param phoneNumber - Raw phone number + * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition + * with the matching positions otherwise + */ + public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber) { + return matchesNumber(context, phoneNumber, mQuery); + } + + /** + * Matches a phone number against a query, taking care of formatting characters + * + * @param phoneNumber - Raw phone number + * @param query - Normalized query (only contains numbers from 0-9) + * @param offset - The position in the number to start the match against (used to ignore leading + * prefixes/country codes) + * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition + * with the matching positions otherwise + */ + private SmartDialMatchPosition matchesNumberWithOffset( + Context context, String phoneNumber, String query, int offset) { + if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(query)) { + return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(offset, offset) : null; + } + int queryAt = 0; + int numberAt = offset; + for (int i = offset; i < phoneNumber.length(); i++) { + if (queryAt == query.length()) { + break; + } + char ch = phoneNumber.charAt(i); + if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) { + if (ch != query.charAt(queryAt)) { + return null; + } + queryAt++; + } else { + if (queryAt == 0) { + // Found a separator before any part of the query was matched, so advance the + // offset to avoid prematurely highlighting separators before the rest of the + // query. + // E.g. don't highlight the first '-' if we're matching 1-510-111-1111 with + // '510'. + // However, if the current offset is 0, just include the beginning separators + // anyway, otherwise the highlighting ends up looking weird. + // E.g. if we're matching (510)-111-1111 with '510', we should include the + // first '('. + if (offset != 0) { + offset++; + } + } + } + numberAt++; + } + return new SmartDialMatchPosition(0 + offset, numberAt); + } + + /** + * This function iterates through each token in the display name, trying to match the query to the + * numeric equivalent of the token. + * + *

      A token is defined as a range in the display name delimited by characters that have no latin + * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters + * - '王'). Transliteration from non-latin characters to latin character will be done on a best + * effort basis - e.g. 'Ü' - 'u'. + * + *

      For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips", + * "thomas", and "jr". + * + *

      A match must begin at the start of a token. For example, typing 846(Tho) would match + * "Phillips Thomas", but 466(hom) would not. + * + *

      Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred + * S)mith. + * + * @param displayName The normalized(no accented characters) display name we intend to match + * against. + * @param query The string of digits that we want to match the display name to. + * @param matchList An array list of {@link SmartDialMatchPosition}s that we add matched positions + * to. + * @return Returns true if a combination of the tokens in displayName match the query string + * contained in query. If the function returns true, matchList will contain an ArrayList of + * match positions (multiple matches correspond to initial matches). + */ + private boolean matchesCombination( + Context context, + String displayName, + String query, + ArrayList matchList) { + StringBuilder builder = new StringBuilder(); + constructEmptyMask(builder, displayName.length()); + final int nameLength = displayName.length(); + final int queryLength = query.length(); + + if (nameLength < queryLength) { + return false; + } + + if (queryLength == 0) { + return false; + } + + // The current character index in displayName + // E.g. 3 corresponds to 'd' in "Fred Smith" + int nameStart = 0; + + // The current character in the query we are trying to match the displayName against + int queryStart = 0; + + // The start position of the current token we are inspecting + int tokenStart = 0; + + // The number of non-alphabetic characters we've encountered so far in the current match. + // E.g. if we've currently matched 3733764849 to (Fred Smith W)illiam, then the + // seperatorCount should be 2. This allows us to correctly calculate offsets for the match + // positions + int seperatorCount = 0; + + ArrayList partial = new ArrayList(); + // Keep going until we reach the end of displayName + while (nameStart < nameLength && queryStart < queryLength) { + char ch = displayName.charAt(nameStart); + // Strip diacritics from accented characters if any + ch = CompositeSmartDialMap.normalizeCharacter(context, ch); + if (CompositeSmartDialMap.isValidDialpadCharacter(context, ch)) { + if (CompositeSmartDialMap.isValidDialpadAlphabeticChar(context, ch)) { + ch = CompositeSmartDialMap.getDialpadNumericCharacter(context, ch); + } + if (ch != query.charAt(queryStart)) { + // Failed to match the current character in the query. + + // Case 1: Failed to match the first character in the query. Skip to the next + // token since there is no chance of this token matching the query. + + // Case 2: Previous characters in the query matched, but the current character + // failed to match. This happened in the middle of a token. Skip to the next + // token since there is no chance of this token matching the query. + + // Case 3: Previous characters in the query matched, but the current character + // failed to match. This happened right at the start of the current token. In + // this case, we should restart the query and try again with the current token. + // Otherwise, we would fail to match a query like "964"(yog) against a name + // Yo-Yoghurt because the query match would fail on the 3rd character, and + // then skip to the end of the "Yoghurt" token. + + if (queryStart == 0 + || CompositeSmartDialMap.isValidDialpadCharacter( + context, + CompositeSmartDialMap.normalizeCharacter( + context, displayName.charAt(nameStart - 1)))) { + // skip to the next token, in the case of 1 or 2. + while (nameStart < nameLength + && CompositeSmartDialMap.isValidDialpadCharacter( + context, + CompositeSmartDialMap.normalizeCharacter( + context, displayName.charAt(nameStart)))) { + nameStart++; + } + nameStart++; + } + + // Restart the query and set the correct token position + queryStart = 0; + seperatorCount = 0; + tokenStart = nameStart; + } else { + if (queryStart == queryLength - 1) { + + // As much as possible, we prioritize a full token match over a sub token + // one so if we find a full token match, we can return right away + matchList.add( + new SmartDialMatchPosition(tokenStart, queryLength + tokenStart + seperatorCount)); + for (SmartDialMatchPosition match : matchList) { + replaceBitInMask(builder, match); + } + return true; + } else if (ALLOW_INITIAL_MATCH && queryStart < INITIAL_LENGTH_LIMIT) { + // we matched the first character. + // branch off and see if we can find another match with the remaining + // characters in the query string and the remaining tokens + // find the next separator in the query string + int j; + for (j = nameStart; j < nameLength; j++) { + if (!CompositeSmartDialMap.isValidDialpadCharacter( + context, + CompositeSmartDialMap.normalizeCharacter(context, displayName.charAt(j)))) { + break; + } + } + // this means there is at least one character left after the separator + if (j < nameLength - 1) { + final String remainder = displayName.substring(j + 1); + final ArrayList partialTemp = new ArrayList<>(); + if (matchesCombination( + context, remainder, query.substring(queryStart + 1), partialTemp)) { + + // store the list of possible match positions + SmartDialMatchPosition.advanceMatchPositions(partialTemp, j + 1); + partialTemp.add(0, new SmartDialMatchPosition(nameStart, nameStart + 1)); + // we found a partial token match, store the data in a + // temp buffer and return it if we end up not finding a full + // token match + partial = partialTemp; + } + } + } + nameStart++; + queryStart++; + // we matched the current character in the name against one in the query, + // continue and see if the rest of the characters match + } + } else { + // found a separator, we skip this character and continue to the next one + nameStart++; + if (queryStart == 0) { + // This means we found a separator before the start of a token, + // so we should increment the token's start position to reflect its true + // start position + tokenStart = nameStart; + } else { + // Otherwise this separator was found in the middle of a token being matched, + // so increase the separator count + seperatorCount++; + } + } + } + // if we have no complete match at this point, then we attempt to fall back to the partial + // token match(if any). If we don't allow initial matching (ALLOW_INITIAL_MATCH = false) + // then partial will always be empty. + if (!partial.isEmpty()) { + matchList.addAll(partial); + for (SmartDialMatchPosition match : matchList) { + replaceBitInMask(builder, match); + } + return true; + } + return false; + } + + /** + * This function iterates through each token in the display name, trying to match the query to the + * numeric equivalent of the token. + * + *

      A token is defined as a range in the display name delimited by characters that have no latin + * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters + * - '王'). Transliteration from non-latin characters to latin character will be done on a best + * effort basis - e.g. 'Ü' - 'u'. + * + *

      For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips", + * "thomas", and "jr". + * + *

      A match must begin at the start of a token. For example, typing 846(Tho) would match + * "Phillips Thomas", but 466(hom) would not. + * + *

      Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred + * S)mith. + * + * @param displayName The normalized(no accented characters) display name we intend to match + * against. + * @return Returns true if a combination of the tokens in displayName match the query string + * contained in query. If the function returns true, matchList will contain an ArrayList of + * match positions (multiple matches correspond to initial matches). + */ + public boolean matches(Context context, String displayName) { + mMatchPositions.clear(); + return matchesCombination(context, displayName, mQuery, mMatchPositions); + } + + public ArrayList getMatchPositions() { + // Return a clone of mMatchPositions so that the caller can use it without + // worrying about it changing + return new ArrayList<>(mMatchPositions); + } + + public String getQuery() { + return mQuery; + } + + public void setQuery(String query) { + mQuery = query; + } + + public void setShouldMatchEmptyQuery(boolean matches) { + mShouldMatchEmptyQuery = matches; + } +} diff --git a/java/com/android/dialer/smartdial/util/SmartDialPrefix.java b/java/com/android/dialer/smartdial/util/SmartDialPrefix.java new file mode 100644 index 000000000..9af411913 --- /dev/null +++ b/java/com/android/dialer/smartdial/util/SmartDialPrefix.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2013 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.smartdial.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.VisibleForTesting; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import com.android.dialer.smartdial.map.CompositeSmartDialMap; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported + * prefix combinations for contact names, and also methods to find supported prefix combinations for + * contacts' phone numbers. Each contact name is separated into several tokens, such as first name, + * middle name, family name etc. Each phone number is also separated into country code, NANP area + * code, and local number if such separation is possible. + */ +public class SmartDialPrefix { + + /** + * The number of starting and ending tokens in a contact's name considered for initials. For + * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed + * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced + * by their initials in contact name matching. Users can look up this contact by combinations of + * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such + * as "CF" "DF" "ACF" "ADF" etc. + */ + private static final int LAST_TOKENS_FOR_INITIALS = 2; + + private static final int FIRST_TOKENS_FOR_INITIALS = 2; + + /** The country code of the user's sim card obtained by calling getSimCountryIso */ + private static final String PREF_USER_SIM_COUNTRY_CODE = + "DialtactsActivity_user_sim_country_code"; + + private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null; + + private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT; + /** Indicates whether user is in NANP regions. */ + private static boolean sUserInNanpRegion = false; + /** Set of country names that use NANP code. */ + private static Set sNanpCountries = null; + /** Set of supported country codes in front of the phone number. */ + private static Set sCountryCodes = null; + + private static boolean sNanpInitialized = false; + + /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */ + public static void initializeNanpSettings(Context context) { + final TelephonyManager manager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (manager != null) { + sUserSimCountryCode = manager.getSimCountryIso(); + } + + final SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + + if (sUserSimCountryCode != null) { + /** Updates shared preferences with the latest country obtained from getSimCountryIso. */ + prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply(); + } else { + /** Uses previously stored country code if loading fails. */ + sUserSimCountryCode = + prefs.getString(PREF_USER_SIM_COUNTRY_CODE, PREF_USER_SIM_COUNTRY_CODE_DEFAULT); + } + /** Queries the NANP country list to find out whether user is in a NANP region. */ + sUserInNanpRegion = isCountryNanp(sUserSimCountryCode); + sNanpInitialized = true; + } + + /** + * Parses a contact's name into a list of separated tokens. + * + * @param contactName Contact's name stored in string. + * @return A list of name tokens, for example separated first names, last name, etc. + */ + public static ArrayList parseToIndexTokens(Context context, String contactName) { + final int length = contactName.length(); + final ArrayList result = new ArrayList<>(); + char c; + final StringBuilder currentIndexToken = new StringBuilder(); + /** + * Iterates through the whole name string. If the current character is a valid character, append + * it to the current token. If the current character is not a valid character, for example space + * " ", mark the current token as complete and add it to the list of tokens. + */ + for (int i = 0; i < length; i++) { + c = CompositeSmartDialMap.normalizeCharacter(context, contactName.charAt(i)); + if (CompositeSmartDialMap.isValidDialpadCharacter(context, c)) { + /** Converts a character into the number on dialpad that represents the character. */ + currentIndexToken.append(CompositeSmartDialMap.getDialpadIndex(context, c)); + } else { + if (currentIndexToken.length() != 0) { + result.add(currentIndexToken.toString()); + } + currentIndexToken.delete(0, currentIndexToken.length()); + } + } + + /** Adds the last token in case it has not been added. */ + if (currentIndexToken.length() != 0) { + result.add(currentIndexToken.toString()); + } + return result; + } + + /** + * Generates a list of strings that any prefix of any string in the list can be used to look up + * the contact's name. + * + * @param index The contact's name in string. + * @return A List of strings, whose prefix can be used to look up the contact. + */ + public static ArrayList generateNamePrefixes(Context context, String index) { + final ArrayList result = new ArrayList<>(); + + /** Parses the name into a list of tokens. */ + final ArrayList indexTokens = parseToIndexTokens(context, index); + + if (indexTokens.size() > 0) { + /** + * Adds the full token combinations to the list. For example, a contact with name "Albert Ben + * Ed Foster" can be looked up by any prefix of the following strings "Foster" "EdFoster" + * "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of look up that contains only + * one token, and that spans multiple continuous tokens. + */ + final StringBuilder fullNameToken = new StringBuilder(); + for (int i = indexTokens.size() - 1; i >= 0; i--) { + fullNameToken.insert(0, indexTokens.get(i)); + result.add(fullNameToken.toString()); + } + + /** + * Adds initial combinations to the list, with the number of initials restricted by {@link + * #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}. For example, a contact + * with name "Albert Ben Ed Foster" can be looked up by any prefix of the following strings + * "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster" "AEFoster" and "ABEFoster". This covers + * all cases of initial lookup. + */ + ArrayList fullNames = new ArrayList<>(); + fullNames.add(indexTokens.get(indexTokens.size() - 1)); + final int recursiveNameStart = result.size(); + int recursiveNameEnd = result.size(); + String initial = ""; + for (int i = indexTokens.size() - 2; i >= 0; i--) { + if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS) + || (i < FIRST_TOKENS_FOR_INITIALS)) { + initial = indexTokens.get(i).substring(0, 1); + + /** Recursively adds initial combinations to the list. */ + for (int j = 0; j < fullNames.size(); ++j) { + result.add(initial + fullNames.get(j)); + } + for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) { + result.add(initial + result.get(j)); + } + recursiveNameEnd = result.size(); + final String currentFullName = fullNames.get(fullNames.size() - 1); + fullNames.add(indexTokens.get(i) + currentFullName); + } + } + } + + return result; + } + + /** + * Computes a list of number strings based on tokens of a given phone number. Any prefix of any + * string in the list can be used to look up the phone number. The list include the full phone + * number, the national number if there is a country code in the phone number, and the local + * number if there is an area code in the phone number following the NANP format. For example, if + * a user has phone number +41 71 394 8392, the list will contain 41713948392 and 713948392. Any + * prefix to either of the strings can be used to look up the phone number. If a user has a phone + * number +1 555-302-3029 (NANP format), the list will contain 15553023029, 5553023029, and + * 3023029. + * + * @param number String of user's phone number. + * @return A list of strings where any prefix of any entry can be used to look up the number. + */ + public static ArrayList parseToNumberTokens(Context context, String number) { + final ArrayList result = new ArrayList<>(); + if (!TextUtils.isEmpty(number)) { + /** Adds the full number to the list. */ + result.add(SmartDialNameMatcher.normalizeNumber(context, number)); + + final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(context, number); + if (phoneNumberTokens == null) { + return result; + } + + if (phoneNumberTokens.countryCodeOffset != 0) { + result.add( + SmartDialNameMatcher.normalizeNumber( + context, number, phoneNumberTokens.countryCodeOffset)); + } + + if (phoneNumberTokens.nanpCodeOffset != 0) { + result.add( + SmartDialNameMatcher.normalizeNumber( + context, number, phoneNumberTokens.nanpCodeOffset)); + } + } + return result; + } + + /** + * Parses a phone number to find out whether it has country code and NANP area code. + * + * @param number Raw phone number. + * @return a PhoneNumberToken instance with country code, NANP code information. + */ + public static PhoneNumberTokens parsePhoneNumber(Context context, String number) { + String countryCode = ""; + int countryCodeOffset = 0; + int nanpNumberOffset = 0; + + if (!TextUtils.isEmpty(number)) { + String normalizedNumber = SmartDialNameMatcher.normalizeNumber(context, number); + if (number.charAt(0) == '+') { + /** If the number starts with '+', tries to find valid country code. */ + for (int i = 1; i <= 1 + 3; i++) { + if (number.length() <= i) { + break; + } + countryCode = number.substring(1, i); + if (isValidCountryCode(countryCode)) { + countryCodeOffset = i; + break; + } + } + } else { + /** + * If the number does not start with '+', finds out whether it is in NANP format and has '1' + * preceding the number. + */ + if ((normalizedNumber.length() == 11) + && (normalizedNumber.charAt(0) == '1') + && (sUserInNanpRegion)) { + countryCode = "1"; + countryCodeOffset = number.indexOf(normalizedNumber.charAt(1)); + if (countryCodeOffset == -1) { + countryCodeOffset = 0; + } + } + } + + /** If user is in NANP region, finds out whether a number is in NANP format. */ + if (sUserInNanpRegion) { + String areaCode = ""; + if (countryCode.equals("") && normalizedNumber.length() == 10) { + /** + * if the number has no country code but fits the NANP format, extracts the NANP area + * code, and finds out offset of the local number. + */ + areaCode = normalizedNumber.substring(0, 3); + } else if (countryCode.equals("1") && normalizedNumber.length() == 11) { + /** + * If the number has country code '1', finds out area code and offset of the local number. + */ + areaCode = normalizedNumber.substring(1, 4); + } + if (!areaCode.equals("")) { + final int areaCodeIndex = number.indexOf(areaCode); + if (areaCodeIndex != -1) { + nanpNumberOffset = number.indexOf(areaCode) + 3; + } + } + } + } + return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset); + } + + /** Checkes whether a country code is valid. */ + private static boolean isValidCountryCode(String countryCode) { + if (sCountryCodes == null) { + sCountryCodes = initCountryCodes(); + } + return sCountryCodes.contains(countryCode); + } + + private static Set initCountryCodes() { + final HashSet result = new HashSet(); + result.add("1"); + result.add("7"); + result.add("20"); + result.add("27"); + result.add("30"); + result.add("31"); + result.add("32"); + result.add("33"); + result.add("34"); + result.add("36"); + result.add("39"); + result.add("40"); + result.add("41"); + result.add("43"); + result.add("44"); + result.add("45"); + result.add("46"); + result.add("47"); + result.add("48"); + result.add("49"); + result.add("51"); + result.add("52"); + result.add("53"); + result.add("54"); + result.add("55"); + result.add("56"); + result.add("57"); + result.add("58"); + result.add("60"); + result.add("61"); + result.add("62"); + result.add("63"); + result.add("64"); + result.add("65"); + result.add("66"); + result.add("81"); + result.add("82"); + result.add("84"); + result.add("86"); + result.add("90"); + result.add("91"); + result.add("92"); + result.add("93"); + result.add("94"); + result.add("95"); + result.add("98"); + result.add("211"); + result.add("212"); + result.add("213"); + result.add("216"); + result.add("218"); + result.add("220"); + result.add("221"); + result.add("222"); + result.add("223"); + result.add("224"); + result.add("225"); + result.add("226"); + result.add("227"); + result.add("228"); + result.add("229"); + result.add("230"); + result.add("231"); + result.add("232"); + result.add("233"); + result.add("234"); + result.add("235"); + result.add("236"); + result.add("237"); + result.add("238"); + result.add("239"); + result.add("240"); + result.add("241"); + result.add("242"); + result.add("243"); + result.add("244"); + result.add("245"); + result.add("246"); + result.add("247"); + result.add("248"); + result.add("249"); + result.add("250"); + result.add("251"); + result.add("252"); + result.add("253"); + result.add("254"); + result.add("255"); + result.add("256"); + result.add("257"); + result.add("258"); + result.add("260"); + result.add("261"); + result.add("262"); + result.add("263"); + result.add("264"); + result.add("265"); + result.add("266"); + result.add("267"); + result.add("268"); + result.add("269"); + result.add("290"); + result.add("291"); + result.add("297"); + result.add("298"); + result.add("299"); + result.add("350"); + result.add("351"); + result.add("352"); + result.add("353"); + result.add("354"); + result.add("355"); + result.add("356"); + result.add("357"); + result.add("358"); + result.add("359"); + result.add("370"); + result.add("371"); + result.add("372"); + result.add("373"); + result.add("374"); + result.add("375"); + result.add("376"); + result.add("377"); + result.add("378"); + result.add("379"); + result.add("380"); + result.add("381"); + result.add("382"); + result.add("385"); + result.add("386"); + result.add("387"); + result.add("389"); + result.add("420"); + result.add("421"); + result.add("423"); + result.add("500"); + result.add("501"); + result.add("502"); + result.add("503"); + result.add("504"); + result.add("505"); + result.add("506"); + result.add("507"); + result.add("508"); + result.add("509"); + result.add("590"); + result.add("591"); + result.add("592"); + result.add("593"); + result.add("594"); + result.add("595"); + result.add("596"); + result.add("597"); + result.add("598"); + result.add("599"); + result.add("670"); + result.add("672"); + result.add("673"); + result.add("674"); + result.add("675"); + result.add("676"); + result.add("677"); + result.add("678"); + result.add("679"); + result.add("680"); + result.add("681"); + result.add("682"); + result.add("683"); + result.add("685"); + result.add("686"); + result.add("687"); + result.add("688"); + result.add("689"); + result.add("690"); + result.add("691"); + result.add("692"); + result.add("800"); + result.add("808"); + result.add("850"); + result.add("852"); + result.add("853"); + result.add("855"); + result.add("856"); + result.add("870"); + result.add("878"); + result.add("880"); + result.add("881"); + result.add("882"); + result.add("883"); + result.add("886"); + result.add("888"); + result.add("960"); + result.add("961"); + result.add("962"); + result.add("963"); + result.add("964"); + result.add("965"); + result.add("966"); + result.add("967"); + result.add("968"); + result.add("970"); + result.add("971"); + result.add("972"); + result.add("973"); + result.add("974"); + result.add("975"); + result.add("976"); + result.add("977"); + result.add("979"); + result.add("992"); + result.add("993"); + result.add("994"); + result.add("995"); + result.add("996"); + result.add("998"); + return result; + } + + /** + * Indicates whether the given country uses NANP numbers + * + * @param country ISO 3166 country code (case doesn't matter) + * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise + * @see + * https://en.wikipedia.org/wiki/North_American_Numbering_Plan + */ + @VisibleForTesting + public static boolean isCountryNanp(String country) { + if (TextUtils.isEmpty(country)) { + return false; + } + if (sNanpCountries == null) { + sNanpCountries = initNanpCountries(); + } + return sNanpCountries.contains(country.toUpperCase()); + } + + private static Set initNanpCountries() { + final HashSet result = new HashSet(); + result.add("US"); // United States + result.add("CA"); // Canada + result.add("AS"); // American Samoa + result.add("AI"); // Anguilla + result.add("AG"); // Antigua and Barbuda + result.add("BS"); // Bahamas + result.add("BB"); // Barbados + result.add("BM"); // Bermuda + result.add("VG"); // British Virgin Islands + result.add("KY"); // Cayman Islands + result.add("DM"); // Dominica + result.add("DO"); // Dominican Republic + result.add("GD"); // Grenada + result.add("GU"); // Guam + result.add("JM"); // Jamaica + result.add("PR"); // Puerto Rico + result.add("MS"); // Montserrat + result.add("MP"); // Northern Mariana Islands + result.add("KN"); // Saint Kitts and Nevis + result.add("LC"); // Saint Lucia + result.add("VC"); // Saint Vincent and the Grenadines + result.add("TT"); // Trinidad and Tobago + result.add("TC"); // Turks and Caicos Islands + result.add("VI"); // U.S. Virgin Islands + return result; + } + + /** + * Returns whether the user is in a region that uses Nanp format based on the sim location. + * + * @return Whether user is in Nanp region. + */ + public static boolean getUserInNanpRegion() { + return sUserInNanpRegion; + } + + /** Explicitly setting the user Nanp to the given boolean */ + @VisibleForTesting + public static void setUserInNanpRegion(boolean userInNanpRegion) { + sUserInNanpRegion = userInNanpRegion; + } + + /** Class to record phone number parsing information. */ + public static class PhoneNumberTokens { + + /** Country code of the phone number. */ + final String countryCode; + + /** Offset of national number after the country code. */ + final int countryCodeOffset; + + /** Offset of local number after NANP area code. */ + final int nanpCodeOffset; + + public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) { + this.countryCode = countryCode; + this.countryCodeOffset = countryCodeOffset; + this.nanpCodeOffset = nanpCodeOffset; + } + } +} -- cgit v1.2.3 From 126c0e599ea98b639549d91ff99bc4e780d769b5 Mon Sep 17 00:00:00 2001 From: linyuh Date: Thu, 21 Dec 2017 16:17:02 -0800 Subject: Use CHAR_TO_KEY_MAPS in DialpadCharMappings in SmartDialMaps. This way we don't have to manually keep the maps in sync. Bug: 30215380,70633239 Test: Existing unit tests PiperOrigin-RevId: 179871748 Change-Id: Idd4cc1155068bb31c17deae80b98d73363fad770 --- .../dialer/dialpadview/DialpadCharMappings.java | 17 ++++++++ .../smartdial/map/BulgarianSmartDialMap.java | 46 +------------------- .../dialer/smartdial/map/LatinSmartDialMap.java | 41 +----------------- .../dialer/smartdial/map/RussianSmartDialMap.java | 49 +--------------------- .../smartdial/map/UkrainianSmartDialMap.java | 49 +--------------------- 5 files changed, 25 insertions(+), 177 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/dialpadview/DialpadCharMappings.java b/java/com/android/dialer/dialpadview/DialpadCharMappings.java index 03bc2e728..0bb28ae0d 100644 --- a/java/com/android/dialer/dialpadview/DialpadCharMappings.java +++ b/java/com/android/dialer/dialpadview/DialpadCharMappings.java @@ -147,6 +147,23 @@ public class DialpadCharMappings { : null; } + /** + * Returns the character-key map of the provided ISO 639-2 language code. + * + *

      Note: this method is for implementations of {@link + * com.android.dialer.smartdial.map.SmartDialMap} only. {@link #getCharToKeyMap(Context)} should + * be used for all other purposes. + * + *

      It is the caller's responsibility to ensure the language code is valid and a character + * mapping is defined for that language. Otherwise, an exception will be thrown. + */ + public static SimpleArrayMap getCharToKeyMap(String languageCode) { + SimpleArrayMap charToKeyMap = CHAR_TO_KEY_MAPS.get(languageCode); + + return Assert.isNotNull( + charToKeyMap, "No character mappings can be found for language code '%s'", languageCode); + } + /** Returns the default character-key map (the one that uses the Latin alphabet). */ public static SimpleArrayMap getDefaultCharToKeyMap() { return Latin.CHAR_TO_KEY; diff --git a/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java b/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java index cbe6afa97..5be9761a1 100644 --- a/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java +++ b/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java @@ -17,54 +17,12 @@ package com.android.dialer.smartdial.map; import android.support.v4.util.SimpleArrayMap; +import com.android.dialer.dialpadview.DialpadCharMappings; import com.google.common.base.Optional; /** A {@link SmartDialMap} for the Bulgarian alphabet. */ @SuppressWarnings("Guava") final class BulgarianSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - // Reference: https://en.wikipedia.org/wiki/Bulgarian_alphabet - static { - CHAR_TO_KEY_MAP.put('а', '2'); - CHAR_TO_KEY_MAP.put('б', '2'); - CHAR_TO_KEY_MAP.put('в', '2'); - CHAR_TO_KEY_MAP.put('г', '2'); - - CHAR_TO_KEY_MAP.put('д', '3'); - CHAR_TO_KEY_MAP.put('е', '3'); - CHAR_TO_KEY_MAP.put('ж', '3'); - CHAR_TO_KEY_MAP.put('з', '3'); - - CHAR_TO_KEY_MAP.put('и', '4'); - CHAR_TO_KEY_MAP.put('й', '4'); - CHAR_TO_KEY_MAP.put('к', '4'); - CHAR_TO_KEY_MAP.put('л', '4'); - - CHAR_TO_KEY_MAP.put('м', '5'); - CHAR_TO_KEY_MAP.put('н', '5'); - CHAR_TO_KEY_MAP.put('о', '5'); - - CHAR_TO_KEY_MAP.put('п', '6'); - CHAR_TO_KEY_MAP.put('р', '6'); - CHAR_TO_KEY_MAP.put('с', '6'); - - CHAR_TO_KEY_MAP.put('т', '7'); - CHAR_TO_KEY_MAP.put('у', '7'); - CHAR_TO_KEY_MAP.put('ф', '7'); - CHAR_TO_KEY_MAP.put('х', '7'); - - CHAR_TO_KEY_MAP.put('ц', '8'); - CHAR_TO_KEY_MAP.put('ч', '8'); - CHAR_TO_KEY_MAP.put('ш', '8'); - CHAR_TO_KEY_MAP.put('щ', '8'); - - CHAR_TO_KEY_MAP.put('ъ', '9'); - CHAR_TO_KEY_MAP.put('ь', '9'); - CHAR_TO_KEY_MAP.put('ю', '9'); - CHAR_TO_KEY_MAP.put('я', '9'); - } private static BulgarianSmartDialMap instance; @@ -86,6 +44,6 @@ final class BulgarianSmartDialMap extends SmartDialMap { @Override SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; + return DialpadCharMappings.getCharToKeyMap("bul"); } } diff --git a/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java index 052af05c0..b8ef951c5 100644 --- a/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java +++ b/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java @@ -17,49 +17,12 @@ package com.android.dialer.smartdial.map; import android.support.v4.util.SimpleArrayMap; +import com.android.dialer.dialpadview.DialpadCharMappings; import com.google.common.base.Optional; /** A {@link SmartDialMap} for the Latin alphabet, which is for T9 dialpad searching. */ @SuppressWarnings("Guava") final class LatinSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - static { - CHAR_TO_KEY_MAP.put('a', '2'); - CHAR_TO_KEY_MAP.put('b', '2'); - CHAR_TO_KEY_MAP.put('c', '2'); - - CHAR_TO_KEY_MAP.put('d', '3'); - CHAR_TO_KEY_MAP.put('e', '3'); - CHAR_TO_KEY_MAP.put('f', '3'); - - CHAR_TO_KEY_MAP.put('g', '4'); - CHAR_TO_KEY_MAP.put('h', '4'); - CHAR_TO_KEY_MAP.put('i', '4'); - - CHAR_TO_KEY_MAP.put('j', '5'); - CHAR_TO_KEY_MAP.put('k', '5'); - CHAR_TO_KEY_MAP.put('l', '5'); - - CHAR_TO_KEY_MAP.put('m', '6'); - CHAR_TO_KEY_MAP.put('n', '6'); - CHAR_TO_KEY_MAP.put('o', '6'); - - CHAR_TO_KEY_MAP.put('p', '7'); - CHAR_TO_KEY_MAP.put('q', '7'); - CHAR_TO_KEY_MAP.put('r', '7'); - CHAR_TO_KEY_MAP.put('s', '7'); - - CHAR_TO_KEY_MAP.put('t', '8'); - CHAR_TO_KEY_MAP.put('u', '8'); - CHAR_TO_KEY_MAP.put('v', '8'); - - CHAR_TO_KEY_MAP.put('w', '9'); - CHAR_TO_KEY_MAP.put('x', '9'); - CHAR_TO_KEY_MAP.put('y', '9'); - CHAR_TO_KEY_MAP.put('z', '9'); - } private static LatinSmartDialMap instance; @@ -780,6 +743,6 @@ final class LatinSmartDialMap extends SmartDialMap { @Override SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; + return DialpadCharMappings.getDefaultCharToKeyMap(); } } diff --git a/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java b/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java index 5038520c2..c10bbb0ce 100644 --- a/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java +++ b/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java @@ -17,57 +17,12 @@ package com.android.dialer.smartdial.map; import android.support.v4.util.SimpleArrayMap; +import com.android.dialer.dialpadview.DialpadCharMappings; import com.google.common.base.Optional; /** A {@link SmartDialMap} for the Russian alphabet. */ @SuppressWarnings("Guava") final class RussianSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - // Reference: https://en.wikipedia.org/wiki/Russian_alphabet - static { - CHAR_TO_KEY_MAP.put('а', '2'); - CHAR_TO_KEY_MAP.put('б', '2'); - CHAR_TO_KEY_MAP.put('в', '2'); - CHAR_TO_KEY_MAP.put('г', '2'); - - CHAR_TO_KEY_MAP.put('д', '3'); - CHAR_TO_KEY_MAP.put('е', '3'); - CHAR_TO_KEY_MAP.put('ё', '3'); - CHAR_TO_KEY_MAP.put('ж', '3'); - CHAR_TO_KEY_MAP.put('з', '3'); - - CHAR_TO_KEY_MAP.put('и', '4'); - CHAR_TO_KEY_MAP.put('й', '4'); - CHAR_TO_KEY_MAP.put('к', '4'); - CHAR_TO_KEY_MAP.put('л', '4'); - - CHAR_TO_KEY_MAP.put('м', '5'); - CHAR_TO_KEY_MAP.put('н', '5'); - CHAR_TO_KEY_MAP.put('о', '5'); - CHAR_TO_KEY_MAP.put('п', '5'); - - CHAR_TO_KEY_MAP.put('р', '6'); - CHAR_TO_KEY_MAP.put('с', '6'); - CHAR_TO_KEY_MAP.put('т', '6'); - CHAR_TO_KEY_MAP.put('у', '6'); - - CHAR_TO_KEY_MAP.put('ф', '7'); - CHAR_TO_KEY_MAP.put('х', '7'); - CHAR_TO_KEY_MAP.put('ц', '7'); - CHAR_TO_KEY_MAP.put('ч', '7'); - - CHAR_TO_KEY_MAP.put('ш', '8'); - CHAR_TO_KEY_MAP.put('щ', '8'); - CHAR_TO_KEY_MAP.put('ъ', '8'); - CHAR_TO_KEY_MAP.put('ы', '8'); - - CHAR_TO_KEY_MAP.put('ь', '9'); - CHAR_TO_KEY_MAP.put('э', '9'); - CHAR_TO_KEY_MAP.put('ю', '9'); - CHAR_TO_KEY_MAP.put('я', '9'); - } private static RussianSmartDialMap instance; @@ -89,6 +44,6 @@ final class RussianSmartDialMap extends SmartDialMap { @Override SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; + return DialpadCharMappings.getCharToKeyMap("rus"); } } diff --git a/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java b/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java index 28dbc0d61..844732c32 100644 --- a/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java +++ b/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java @@ -17,56 +17,11 @@ package com.android.dialer.smartdial.map; import android.support.v4.util.SimpleArrayMap; +import com.android.dialer.dialpadview.DialpadCharMappings; import com.google.common.base.Optional; /** A {@link SmartDialMap} for the Ukrainian alphabet. */ final class UkrainianSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - // Reference: https://en.wikipedia.org/wiki/Ukrainian_alphabet - static { - CHAR_TO_KEY_MAP.put('а', '2'); - CHAR_TO_KEY_MAP.put('б', '2'); - CHAR_TO_KEY_MAP.put('в', '2'); - CHAR_TO_KEY_MAP.put('г', '2'); - CHAR_TO_KEY_MAP.put('ґ', '2'); - - CHAR_TO_KEY_MAP.put('д', '3'); - CHAR_TO_KEY_MAP.put('е', '3'); - CHAR_TO_KEY_MAP.put('є', '3'); - CHAR_TO_KEY_MAP.put('ж', '3'); - CHAR_TO_KEY_MAP.put('з', '3'); - - CHAR_TO_KEY_MAP.put('и', '4'); - CHAR_TO_KEY_MAP.put('і', '4'); - CHAR_TO_KEY_MAP.put('ї', '4'); - CHAR_TO_KEY_MAP.put('й', '4'); - CHAR_TO_KEY_MAP.put('к', '4'); - CHAR_TO_KEY_MAP.put('л', '4'); - - CHAR_TO_KEY_MAP.put('м', '5'); - CHAR_TO_KEY_MAP.put('н', '5'); - CHAR_TO_KEY_MAP.put('о', '5'); - CHAR_TO_KEY_MAP.put('п', '5'); - - CHAR_TO_KEY_MAP.put('р', '6'); - CHAR_TO_KEY_MAP.put('с', '6'); - CHAR_TO_KEY_MAP.put('т', '6'); - CHAR_TO_KEY_MAP.put('у', '6'); - - CHAR_TO_KEY_MAP.put('ф', '7'); - CHAR_TO_KEY_MAP.put('х', '7'); - CHAR_TO_KEY_MAP.put('ц', '7'); - CHAR_TO_KEY_MAP.put('ч', '7'); - - CHAR_TO_KEY_MAP.put('ш', '8'); - CHAR_TO_KEY_MAP.put('щ', '8'); - - CHAR_TO_KEY_MAP.put('ь', '9'); - CHAR_TO_KEY_MAP.put('ю', '9'); - CHAR_TO_KEY_MAP.put('я', '9'); - } private static UkrainianSmartDialMap instance; @@ -88,6 +43,6 @@ final class UkrainianSmartDialMap extends SmartDialMap { @Override SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; + return DialpadCharMappings.getCharToKeyMap("ukr"); } } -- cgit v1.2.3 From 6aa961b5fa429ad64a9c0bf02d3a1673950ef743 Mon Sep 17 00:00:00 2001 From: uabdullah Date: Thu, 21 Dec 2017 16:28:55 -0800 Subject: Differentiate read/unread voicemails in the NUI Voicemail by bolding. Voicemails that are unread in the annotated call log table will show up as bold. Voicemails that are marked as read will show up as normal (non bold). A follow up CL will update the underlying table and mark them as read when a viewholder is expanded. Bug: 64882313,70900195 Test: Unit Tests PiperOrigin-RevId: 179872932 Change-Id: I927711aa8c6c6324e43f519c14a58b5f2b8e7ca9 --- .../voicemail/listui/NewVoicemailViewHolder.java | 22 ++++++++++++++++++++++ .../voicemail/listui/VoicemailCursorLoader.java | 5 ++++- .../dialer/voicemail/model/VoicemailEntry.java | 7 ++++++- 3 files changed, 32 insertions(+), 2 deletions(-) (limited to 'java/com/android') diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java index 24bed0f04..dac4ebafc 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java @@ -21,6 +21,7 @@ import static android.view.View.VISIBLE; import android.app.FragmentManager; import android.content.Context; import android.database.Cursor; +import android.graphics.Typeface; import android.net.Uri; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; @@ -124,6 +125,9 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On transcriptionTextView.setText(voicemailTranscription); } + // Bold if voicemail is unread + boldViewHolderIfUnread(); + itemView.setOnClickListener(this); menuButton.setOnClickListener( NewVoicemailMenu.createOnClickListener(context, voicemailEntryOfViewHolder)); @@ -173,6 +177,20 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On mediaPlayerView.getVisibility() == VISIBLE); } + private void boldViewHolderIfUnread() { + LogUtil.v( + "NewVoicemailViewHolder.boldViewHolderIfUnread", + "id:%d, isRead:%d", + voicemailEntryOfViewHolder.id(), + voicemailEntryOfViewHolder.isRead()); + + if (voicemailEntryOfViewHolder.isRead() == 0) { + primaryTextView.setTypeface(null, Typeface.BOLD); + secondaryTextView.setTypeface(null, Typeface.BOLD); + transcriptionTextView.setTypeface(null, Typeface.BOLD); + } + } + // TODO(uabdullah): Consider/Implement TYPE (e.g Spam, TYPE_VOICEMAIL) private void setPhoto(VoicemailEntry voicemailEntry) { ContactPhotoManager.getInstance(context) @@ -214,6 +232,10 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On isViewHolderExpanded = false; viewHolderVoicemailUri = null; + primaryTextView.setTypeface(null, Typeface.NORMAL); + secondaryTextView.setTypeface(null, Typeface.NORMAL); + transcriptionTextView.setTypeface(null, Typeface.NORMAL); + mediaPlayerView.reset(); LogUtil.i( diff --git a/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java b/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java index 6a55483a4..55d36b364 100644 --- a/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java +++ b/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java @@ -43,7 +43,8 @@ final class VoicemailCursorLoader extends CursorLoader { AnnotatedCallLog.GEOCODED_LOCATION, AnnotatedCallLog.CALL_TYPE, AnnotatedCallLog.TRANSCRIPTION, - AnnotatedCallLog.VOICEMAIL_URI + AnnotatedCallLog.VOICEMAIL_URI, + AnnotatedCallLog.IS_READ }; // Indexes for VOICEMAIL_COLUMNS @@ -60,6 +61,7 @@ final class VoicemailCursorLoader extends CursorLoader { private static final int CALL_TYPE = 10; private static final int TRANSCRIPTION = 11; private static final int VOICEMAIL_URI = 12; + private static final int IS_READ = 13; // TODO(zachh): Optimize indexes VoicemailCursorLoader(Context context) { @@ -95,6 +97,7 @@ final class VoicemailCursorLoader extends CursorLoader { .setVoicemailUri(cursor.getString(VOICEMAIL_URI)) .setGeocodedLocation(cursor.getString(GEOCODED_LOCATION)) .setCallType(cursor.getInt(CALL_TYPE)) + .setIsRead(cursor.getInt(IS_READ)) .build(); } diff --git a/java/com/android/dialer/voicemail/model/VoicemailEntry.java b/java/com/android/dialer/voicemail/model/VoicemailEntry.java index df30dee9c..702f52d17 100644 --- a/java/com/android/dialer/voicemail/model/VoicemailEntry.java +++ b/java/com/android/dialer/voicemail/model/VoicemailEntry.java @@ -32,7 +32,8 @@ public abstract class VoicemailEntry { .setNumber(DialerPhoneNumber.getDefaultInstance()) .setPhotoId(0) .setDuration(0) - .setCallType(0); + .setCallType(0) + .setIsRead(0); } public abstract int id(); @@ -69,6 +70,8 @@ public abstract class VoicemailEntry { public abstract int callType(); + public abstract int isRead(); + /** Builder for {@link VoicemailEntry}. */ @AutoValue.Builder public abstract static class Builder { @@ -99,6 +102,8 @@ public abstract class VoicemailEntry { public abstract Builder setCallType(int callType); + public abstract Builder setIsRead(int isRead); + public abstract VoicemailEntry build(); } } -- cgit v1.2.3 From ca5c556f68d798fe7978a20d8f6c53166abfd15b Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Thu, 21 Dec 2017 16:33:14 -0800 Subject: Automated rollback of changelist 179847039 Bug: 36841782 Test: tap PiperOrigin-RevId: 179873378 Change-Id: I5e579f11c9738a75f312be33de556fd2bb3dac42 --- java/com/android/dialer/app/res/values/colors.xml | 1 + .../dialer/speeddial/FavoritesViewHolder.java | 56 ++--------------- .../dialer/speeddial/SpeedDialFragment.java | 14 +---- .../res/drawable/context_menu_background.xml | 25 -------- .../speeddial/res/layout/favorite_context_menu.xml | 72 ---------------------- .../speeddial/res/layout/fragment_speed_dial.xml | 4 +- .../android/dialer/speeddial/res/values/dimens.xml | 15 ----- .../dialer/speeddial/res/values/strings.xml | 15 ----- .../android/dialer/speeddial/res/values/styles.xml | 27 -------- .../com/android/dialer/theme/res/values/dimens.xml | 3 - 10 files changed, 10 insertions(+), 222 deletions(-) delete mode 100644 java/com/android/dialer/speeddial/res/drawable/context_menu_background.xml delete mode 100644 java/com/android/dialer/speeddial/res/layout/favorite_context_menu.xml delete mode 100644 java/com/android/dialer/speeddial/res/values/styles.xml (limited to 'java/com/android') diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml index bb3662cda..84a381f21 100644 --- a/java/com/android/dialer/app/res/values/colors.xml +++ b/java/com/android/dialer/app/res/values/colors.xml @@ -68,6 +68,7 @@ @color/dialer_theme_color_20pct #eeeeee + #D8D8D8 @color/dialer_primary_text_color diff --git a/java/com/android/dialer/speeddial/FavoritesViewHolder.java b/java/com/android/dialer/speeddial/FavoritesViewHolder.java index 4e8d4ad07..f9eec8e79 100644 --- a/java/com/android/dialer/speeddial/FavoritesViewHolder.java +++ b/java/com/android/dialer/speeddial/FavoritesViewHolder.java @@ -22,16 +22,12 @@ import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; -import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; -import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.widget.FrameLayout; -import android.widget.LinearLayout.LayoutParams; -import android.widget.PopupWindow; import android.widget.QuickContactBadge; import android.widget.TextView; import com.android.dialer.common.Assert; @@ -48,9 +44,6 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder private final TextView nameView; private final TextView phoneType; private final FrameLayout videoCallIcon; - private final View contextMenuAnchor; - - private PopupWindow contextMenu; private boolean hasDefaultNumber; private boolean isVideoCall; @@ -63,7 +56,6 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder nameView = view.findViewById(R.id.name); phoneType = view.findViewById(R.id.phone_type); videoCallIcon = view.findViewById(R.id.video_call_container); - contextMenuAnchor = view.findViewById(R.id.avatar_container); view.setOnClickListener(this); view.setOnLongClickListener(this); photoView.setClickable(false); @@ -120,44 +112,12 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder } @Override - public boolean onLongClick(View view) { - Context context = itemView.getContext(); - View contentView = LayoutInflater.from(context).inflate(R.layout.favorite_context_menu, null); - contentView - .findViewById(R.id.voice_call_container) - .setOnClickListener(v -> listener.onClick(Assert.isNotNull(number), false)); - contentView - .findViewById(R.id.video_call_container) - .setOnClickListener(v -> listener.onClick(Assert.isNotNull(number), true)); - contentView - .findViewById(R.id.send_message_container) - .setOnClickListener(v -> listener.openSmsConversation(Assert.isNotNull(number))); - contentView - .findViewById(R.id.remove_container) - .setOnClickListener(v -> listener.removeFavoriteContact()); - contentView - .findViewById(R.id.contact_info_container) - .setOnClickListener(v -> listener.openContactInfo()); - - int offset = - context.getResources().getDimensionPixelSize(R.dimen.speed_dial_context_menu_x_offset); - int padding = - context.getResources().getDimensionPixelSize(R.dimen.speed_dial_context_menu_extra_width); - int width = padding + itemView.getWidth(); - int elevation = context.getResources().getDimensionPixelSize(R.dimen.context_menu_elevation); - contextMenu = new PopupWindow(contentView, width, LayoutParams.WRAP_CONTENT, true); - contextMenu.setBackgroundDrawable(context.getDrawable(R.drawable.context_menu_background)); - contextMenu.setElevation(elevation); - contextMenu.setOnDismissListener(() -> contextMenu = null); - contextMenu.showAsDropDown(contextMenuAnchor, offset, 0); + public boolean onLongClick(View v) { + // TODO(calderwoodra): implement drag and drop logic + listener.onLongClick(number); return true; } - @VisibleForTesting - public PopupWindow getContextMenu() { - return contextMenu; - } - /** Listener/callback for {@link FavoritesViewHolder} actions. */ public interface FavoriteContactsListener { @@ -167,13 +127,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder /** Called when the user clicks on a favorite contact. */ void onClick(String number, boolean isVideoCall); - /** Called when the user selects send message from the context menu. */ - void openSmsConversation(String number); - - /** Called when the user selects remove from the context menu. */ - void removeFavoriteContact(); - - /** Called when the user selects contact info from the context menu. */ - void openContactInfo(); + /** Called when the user long clicks on a favorite contact. */ + void onLongClick(String number); } } diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java index a75c5ddbd..979c894fe 100644 --- a/java/com/android/dialer/speeddial/SpeedDialFragment.java +++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java @@ -112,18 +112,8 @@ public class SpeedDialFragment extends Fragment { } @Override - public void openSmsConversation(String number) { - // TODO(calderwoodra): open sms conversation - } - - @Override - public void removeFavoriteContact() { - // TODO(calderwoodra): remove contact from favorites - } - - @Override - public void openContactInfo() { - // TODO(calderwoodra): open quick contact info + public void onLongClick(String number) { + // TODO(calderwoodra): show favorite contact floating context menu } } diff --git a/java/com/android/dialer/speeddial/res/drawable/context_menu_background.xml b/java/com/android/dialer/speeddial/res/drawable/context_menu_background.xml deleted file mode 100644 index fd60757b5..000000000 --- a/java/com/android/dialer/speeddial/res/drawable/context_menu_background.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/java/com/android/dialer/speeddial/res/layout/favorite_context_menu.xml b/java/com/android/dialer/speeddial/res/layout/favorite_context_menu.xml deleted file mode 100644 index 04d0139e0..000000000 --- a/java/com/android/dialer/speeddial/res/layout/favorite_context_menu.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml b/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml index b4fc9c08d..d432f097b 100644 --- a/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml +++ b/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml @@ -19,6 +19,6 @@ android:id="@+id/speed_dial_recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingStart="@dimen/speed_dial_recyclerview_horizontal_padding" - android:paddingEnd="@dimen/speed_dial_recyclerview_horizontal_padding" + android:paddingStart="16dp" + android:paddingEnd="16dp" android:clipToPadding="false"/> diff --git a/java/com/android/dialer/speeddial/res/values/dimens.xml b/java/com/android/dialer/speeddial/res/values/dimens.xml index ce2de9dcc..74b509b73 100644 --- a/java/com/android/dialer/speeddial/res/values/dimens.xml +++ b/java/com/android/dialer/speeddial/res/values/dimens.xml @@ -16,19 +16,4 @@ --> 280dp - 200dp - 4dp - 16dp - - - 28dp - - -24dp - 16dp \ No newline at end of file diff --git a/java/com/android/dialer/speeddial/res/values/strings.xml b/java/com/android/dialer/speeddial/res/values/strings.xml index 666eedea9..677f772c5 100644 --- a/java/com/android/dialer/speeddial/res/values/strings.xml +++ b/java/com/android/dialer/speeddial/res/values/strings.xml @@ -38,21 +38,6 @@ Call - - Voice Call - - - Video Call - - - Message - - - Remove - - - Contact info - Add Favorite \ No newline at end of file diff --git a/java/com/android/dialer/speeddial/res/values/styles.xml b/java/com/android/dialer/speeddial/res/values/styles.xml deleted file mode 100644 index 83bbd09e5..000000000 --- a/java/com/android/dialer/speeddial/res/values/styles.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - \ No newline at end of file diff --git a/java/com/android/dialer/theme/res/values/dimens.xml b/java/com/android/dialer/theme/res/values/dimens.xml index 52a7560b3..2b5243ebd 100644 --- a/java/com/android/dialer/theme/res/values/dimens.xml +++ b/java/com/android/dialer/theme/res/values/dimens.xml @@ -50,7 +50,4 @@ 72dp - - - #D8D8D8 -- cgit v1.2.3 From 1e8275f3e6f57f285900ce35946064e0550b174a Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Thu, 21 Dec 2017 16:45:00 -0800 Subject: Automated rollback of changelist 179615699 Bug: 36841782 Test: tap PiperOrigin-RevId: 179874507 Change-Id: I6634243136520d7f13bbe3637ed4d890c38bf887 --- .../binary/aosp/AospDialerRootComponent.java | 2 - .../basecomponent/BaseDialerRootComponent.java | 4 +- .../google/GoogleStubDialerRootComponent.java | 2 - .../databasepopulator/ContactsPopulator.java | 21 ------ .../dialer/speeddial/AddFavoriteActivity.java | 12 +--- .../android/dialer/speeddial/DisambigDialog.java | 2 - .../dialer/speeddial/FavoritesViewHolder.java | 2 +- .../android/dialer/speeddial/SpeedDialAdapter.java | 2 +- .../android/dialer/speeddial/SpeedDialCursor.java | 77 ++++++++-------------- .../speeddial/StrequentContactsCursorLoader.java | 39 +++++------ .../speeddial/room/SpeedDialDatabaseComponent.java | 38 ----------- .../speeddial/room/SpeedDialDatabaseModule.java | 35 ---------- .../dialer/speeddial/room/SpeedDialEntry.java | 58 ---------------- .../dialer/speeddial/room/SpeedDialEntryDao.java | 44 ------------- .../speeddial/room/SpeedDialEntryDatabase.java | 55 ---------------- 15 files changed, 48 insertions(+), 345 deletions(-) delete mode 100644 java/com/android/dialer/speeddial/room/SpeedDialDatabaseComponent.java delete mode 100644 java/com/android/dialer/speeddial/room/SpeedDialDatabaseModule.java delete mode 100644 java/com/android/dialer/speeddial/room/SpeedDialEntry.java delete mode 100644 java/com/android/dialer/speeddial/room/SpeedDialEntryDao.java delete mode 100644 java/com/android/dialer/speeddial/room/SpeedDialEntryDatabase.java (limited to 'java/com/android') diff --git a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java index 0e74eff78..c344fad62 100644 --- a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java +++ b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java @@ -30,7 +30,6 @@ import com.android.dialer.precall.impl.PreCallModule; import com.android.dialer.preferredsim.suggestion.stub.StubSimSuggestionModule; import com.android.dialer.simulator.impl.SimulatorModule; import com.android.dialer.spam.StubSpamModule; -import com.android.dialer.speeddial.room.SpeedDialDatabaseModule; import com.android.dialer.storage.StorageModule; import com.android.dialer.strictmode.impl.SystemStrictModeModule; import com.android.incallui.calllocation.stub.StubCallLocationModule; @@ -61,7 +60,6 @@ import javax.inject.Singleton; StubSimSuggestionModule.class, StubFeedbackModule.class, StubSpamModule.class, - SpeedDialDatabaseModule.class, } ) public interface AospDialerRootComponent extends BaseDialerRootComponent {} diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java index 66bd04aa0..8973a329f 100644 --- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java +++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java @@ -30,7 +30,6 @@ import com.android.dialer.precall.PreCallComponent; import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent; import com.android.dialer.simulator.SimulatorComponent; import com.android.dialer.spam.SpamComponent; -import com.android.dialer.speeddial.room.SpeedDialDatabaseComponent; import com.android.dialer.storage.StorageComponent; import com.android.dialer.strictmode.StrictModeComponent; import com.android.incallui.calllocation.CallLocationComponent; @@ -60,5 +59,4 @@ public interface BaseDialerRootComponent StorageComponent.HasComponent, StrictModeComponent.HasComponent, VoicemailComponent.HasComponent, - SpamComponent.HasComponent, - SpeedDialDatabaseComponent.HasComponent {} + SpamComponent.HasComponent {} diff --git a/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java b/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java index 891e477c9..b54e06229 100644 --- a/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java +++ b/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java @@ -30,7 +30,6 @@ import com.android.dialer.precall.impl.PreCallModule; import com.android.dialer.preferredsim.suggestion.stub.StubSimSuggestionModule; import com.android.dialer.simulator.impl.SimulatorModule; import com.android.dialer.spam.StubSpamModule; -import com.android.dialer.speeddial.room.SpeedDialDatabaseModule; import com.android.dialer.storage.StorageModule; import com.android.dialer.strictmode.impl.SystemStrictModeModule; import com.android.incallui.calllocation.impl.CallLocationModule; @@ -56,7 +55,6 @@ import javax.inject.Singleton; StubSimSuggestionModule.class, SharedPrefConfigProviderModule.class, SimulatorModule.class, - SpeedDialDatabaseModule.class, StorageModule.class, SystemStrictModeModule.class, StubEnrichedCallModule.class, diff --git a/java/com/android/dialer/databasepopulator/ContactsPopulator.java b/java/com/android/dialer/databasepopulator/ContactsPopulator.java index 484851826..f22552db7 100644 --- a/java/com/android/dialer/databasepopulator/ContactsPopulator.java +++ b/java/com/android/dialer/databasepopulator/ContactsPopulator.java @@ -32,8 +32,6 @@ import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.text.TextUtils; import com.android.dialer.common.Assert; -import com.android.dialer.speeddial.room.SpeedDialDatabaseComponent; -import com.android.dialer.speeddial.room.SpeedDialEntry; import com.google.auto.value.AutoValue; import java.io.ByteArrayOutputStream; import java.util.ArrayList; @@ -51,7 +49,6 @@ public final class ContactsPopulator { .addEmail(new Email("m@example.com")) .setIsStarred(true) .setPinned(1) - .setContactId(1L) .setOrangePhoto() .build(), // US, contact with a non-e164 number. @@ -61,7 +58,6 @@ public final class ContactsPopulator { .addEmail(new Email("l@example.com")) .setIsStarred(true) .setPinned(2) - .setContactId(2L) .setBluePhoto() .build(), // UK, number where the (0) should be dropped. @@ -71,7 +67,6 @@ public final class ContactsPopulator { .addEmail(new Email("r@example.com")) .setIsStarred(true) .setPinned(3) - .setContactId(3L) .setRedPhoto() .build(), // US and Australia, contact with a long name and multiple phone numbers. @@ -82,7 +77,6 @@ public final class ContactsPopulator { .addPhoneNumber(new PhoneNumber("+61 2 9374 4001", Phone.TYPE_FAX_HOME)) .setIsStarred(true) .setPinned(4) - .setContactId(4L) .setPurplePhoto() .build(), // US, phone number shared with another contact and 2nd phone number with wait and pause. @@ -161,14 +155,6 @@ public final class ContactsPopulator { addContact(SIMPLE_CONTACTS[5], operations); try { context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations); - SpeedDialDatabaseComponent.get(context.getApplicationContext()) - .speedDialDatabase() - .getSpeedDialEntryDao() - .insert( - new SpeedDialEntry( - SIMPLE_CONTACTS[0].getPhoneNumbers().get(0).value, - SIMPLE_CONTACTS[0].getContactId(), - SpeedDialEntry.VOICE)); } catch (RemoteException | OperationApplicationException e) { Assert.fail("error adding contacts: " + e); } @@ -206,7 +192,6 @@ public final class ContactsPopulator { .withValue( ContactsContract.RawContacts.PINNED, contact.getIsStarred() ? contact.getPinned() : 0) - .withValue(ContactsContract.RawContacts.CONTACT_ID, contact.getContactId()) .withYieldAllowed(true) .build()); @@ -277,9 +262,6 @@ public final class ContactsPopulator { abstract int getPinned(); - @NonNull - abstract long getContactId(); - @Nullable abstract ByteArrayOutputStream getPhotoStream(); @@ -295,7 +277,6 @@ public final class ContactsPopulator { .setAccountName("foo@example") .setPinned(0) .setIsStarred(false) - .setContactId(0) .setPhoneNumbers(new ArrayList<>()) .setEmails(new ArrayList<>()); } @@ -315,8 +296,6 @@ public final class ContactsPopulator { abstract Builder setPinned(int position); - abstract Builder setContactId(long contactId); - abstract Builder setPhotoStream(ByteArrayOutputStream photoStream); abstract Builder setPhoneNumbers(@NonNull List phoneNumbers); diff --git a/java/com/android/dialer/speeddial/AddFavoriteActivity.java b/java/com/android/dialer/speeddial/AddFavoriteActivity.java index 93cd536df..eea6e840e 100644 --- a/java/com/android/dialer/speeddial/AddFavoriteActivity.java +++ b/java/com/android/dialer/speeddial/AddFavoriteActivity.java @@ -31,8 +31,6 @@ import com.android.dialer.common.Assert; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.contactsfragment.ContactsFragment; import com.android.dialer.contactsfragment.ContactsFragment.OnContactSelectedListener; -import com.android.dialer.speeddial.room.SpeedDialDatabaseComponent; -import com.android.dialer.speeddial.room.SpeedDialEntry; /** * Activity for selecting a single contact and adding it to favorites. @@ -95,19 +93,13 @@ public class AddFavoriteActivity extends AppCompatActivity implements OnContactS @WorkerThread private int markContactStarred(long contactId) { - // Insert into SpeedDialEntry database - SpeedDialDatabaseComponent.get(getApplicationContext()) - .speedDialDatabase() - .getSpeedDialEntryDao() - .insert(SpeedDialEntry.newSpeedDialEntry(contactId)); - - // Insert into Cp2 + // TODO(calderwoodra): For now, we will just mark contacts as starred. This means that contacts + // will only be able to exist once in favorites until we implement multiple contact avenues. ContentValues contentValues = new ContentValues(); contentValues.put(Contacts.STARRED, 1); String where = Contacts._ID + " = ?"; String[] selectionArgs = new String[] {Long.toString(contactId)}; - return getContentResolver().update(Contacts.CONTENT_URI, contentValues, where, selectionArgs); } diff --git a/java/com/android/dialer/speeddial/DisambigDialog.java b/java/com/android/dialer/speeddial/DisambigDialog.java index 5598d1e48..ca02f41eb 100644 --- a/java/com/android/dialer/speeddial/DisambigDialog.java +++ b/java/com/android/dialer/speeddial/DisambigDialog.java @@ -234,8 +234,6 @@ public class DisambigDialog extends DialogFragment { ArrayList projection = new ArrayList<>(Arrays.asList(LookupContactInfoWorker.projection)); projection.add(Phone.LOOKUP_KEY); - projection.add(Phone.CONTACT_ID); - projection.add(Phone.STARRED); return projection.toArray(new String[projection.size()]); } diff --git a/java/com/android/dialer/speeddial/FavoritesViewHolder.java b/java/com/android/dialer/speeddial/FavoritesViewHolder.java index f9eec8e79..c25b05ead 100644 --- a/java/com/android/dialer/speeddial/FavoritesViewHolder.java +++ b/java/com/android/dialer/speeddial/FavoritesViewHolder.java @@ -122,7 +122,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder public interface FavoriteContactsListener { /** Called when the user clicks on a favorite contact that doesn't have a default number. */ - void onAmbiguousContactClicked(String lookupKey); + void onAmbiguousContactClicked(String contactId); /** Called when the user clicks on a favorite contact. */ void onClick(String number, boolean isVideoCall); diff --git a/java/com/android/dialer/speeddial/SpeedDialAdapter.java b/java/com/android/dialer/speeddial/SpeedDialAdapter.java index b72647550..5f7b68e5c 100644 --- a/java/com/android/dialer/speeddial/SpeedDialAdapter.java +++ b/java/com/android/dialer/speeddial/SpeedDialAdapter.java @@ -90,7 +90,7 @@ final class SpeedDialAdapter extends RecyclerView.Adapter entries) { + public static SpeedDialCursor newInstance(Cursor strequentCursor) { if (strequentCursor == null || strequentCursor.getCount() == 0) { return null; } - SpeedDialCursor cursor = new SpeedDialCursor(buildCursors(strequentCursor, entries)); + SpeedDialCursor cursor = new SpeedDialCursor(buildCursors(strequentCursor)); strequentCursor.close(); return cursor; } - private static Cursor[] buildCursors(Cursor strequentCursor, List entries) { + private static Cursor[] buildCursors(Cursor strequentCursor) { MatrixCursor starred = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); MatrixCursor suggestions = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); - // Build starred cursor - for (SpeedDialEntry entry : entries) { - if (!moveToContactEntry(strequentCursor, entry)) { - LogUtil.e("SpeedDialCursor.buildCursors", "Entry not found: " + entry); - continue; + strequentCursor.moveToPosition(-1); + while (strequentCursor.moveToNext()) { + if (strequentCursor.getPosition() != 0) { + long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID); + int position = strequentCursor.getPosition(); + boolean duplicate = false; + // Iterate backwards through the cursor to check that this isn't a duplicate contact + // TODO(calderwoodra): improve this algorithm (currently O(n^2)). + while (strequentCursor.moveToPrevious() && !duplicate) { + duplicate |= + strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID) == contactId; + } + strequentCursor.moveToPosition(position); + if (duplicate) { + continue; + } } - if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) != 1) { - LogUtil.e("SpeedDialCursor.buildCursors", "SpeedDialEntry contact is no longer starred"); - continue; - } - StrequentContactsCursorLoader.addToCursor(starred, strequentCursor); - } - - // Build suggestions cursor - strequentCursor.moveToFirst(); - Set contactIds = new ArraySet<>(); - do { if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { - // Starred contact - continue; - } - - long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID); - if (!contactIds.add(contactId)) { - // duplicate contact - continue; + StrequentContactsCursorLoader.addToCursor(starred, strequentCursor); + } else if (starred.getCount() + suggestions.getCount() < SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT) { + // Since all starred contacts come before each non-starred contact, it's safe to assume that + // this list will never exceed the soft limit unless there are more starred contacts than + // the limit permits. + StrequentContactsCursorLoader.addToCursor(suggestions, strequentCursor); } - - StrequentContactsCursorLoader.addToCursor(suggestions, strequentCursor); - } while (strequentCursor.moveToNext() - && starred.getCount() + suggestions.getCount() < SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT); + } List cursorList = new ArrayList<>(); - cursorList.add(createHeaderCursor(R.string.favorites_header)); if (starred.getCount() > 0) { + cursorList.add(createHeaderCursor(R.string.favorites_header)); cursorList.add(starred); } if (suggestions.getCount() > 0) { @@ -112,17 +102,6 @@ public final class SpeedDialCursor extends MergeCursor { return cursorList.toArray(new Cursor[cursorList.size()]); } - private static boolean moveToContactEntry(Cursor strequentCursor, SpeedDialEntry entry) { - boolean matchFound; - strequentCursor.moveToFirst(); - do { - long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID); - String number = strequentCursor.getString(StrequentContactsCursorLoader.PHONE_NUMBER); - matchFound = contactId == entry.contactId || Objects.equals(number, entry.number); - } while (!matchFound && strequentCursor.moveToNext()); - return matchFound; - } - private static Cursor createHeaderCursor(@StringRes int header) { MatrixCursor cursor = new MatrixCursor(HEADER_CURSOR_PROJECTION); cursor.newRow().add(HEADER_CURSOR_PROJECTION[HEADER_COLUMN_POSITION], header); diff --git a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java index b8da47562..e9e3e32da 100644 --- a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java +++ b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java @@ -24,27 +24,24 @@ import android.net.Uri; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; -import com.android.dialer.speeddial.room.SpeedDialDatabaseComponent; -import com.android.dialer.speeddial.room.SpeedDialEntry; -import java.util.List; /** Cursor Loader for strequent contacts. */ -public final class StrequentContactsCursorLoader extends CursorLoader { +final class StrequentContactsCursorLoader extends CursorLoader { - public static final int PHONE_ID = 0; - public static final int PHONE_DISPLAY_NAME = 1; - public static final int PHONE_STARRED = 2; - public static final int PHONE_PHOTO_URI = 3; - public static final int PHONE_LOOKUP_KEY = 4; - public static final int PHONE_PHOTO_ID = 5; - public static final int PHONE_NUMBER = 6; - public static final int PHONE_TYPE = 7; - public static final int PHONE_LABEL = 8; - public static final int PHONE_IS_SUPER_PRIMARY = 9; - public static final int PHONE_PINNED = 10; - public static final int PHONE_CONTACT_ID = 11; + static final int PHONE_ID = 0; + static final int PHONE_DISPLAY_NAME = 1; + static final int PHONE_STARRED = 2; + static final int PHONE_PHOTO_URI = 3; + static final int PHONE_LOOKUP_KEY = 4; + static final int PHONE_PHOTO_ID = 5; + static final int PHONE_NUMBER = 6; + static final int PHONE_TYPE = 7; + static final int PHONE_LABEL = 8; + static final int PHONE_IS_SUPER_PRIMARY = 9; + static final int PHONE_PINNED = 10; + static final int PHONE_CONTACT_ID = 11; - public static final String[] PHONE_PROJECTION = + static final String[] PHONE_PROJECTION = new String[] { Phone._ID, // 0 Phone.DISPLAY_NAME, // 1 @@ -95,12 +92,6 @@ public final class StrequentContactsCursorLoader extends CursorLoader { @Override public Cursor loadInBackground() { - Cursor strequentCursor = super.loadInBackground(); - List entries = - SpeedDialDatabaseComponent.get(getContext().getApplicationContext()) - .speedDialDatabase() - .getSpeedDialEntryDao() - .getAllEntries(); - return SpeedDialCursor.newInstance(strequentCursor, entries); + return SpeedDialCursor.newInstance(super.loadInBackground()); } } diff --git a/java/com/android/dialer/speeddial/room/SpeedDialDatabaseComponent.java b/java/com/android/dialer/speeddial/room/SpeedDialDatabaseComponent.java deleted file mode 100644 index c415fd767..000000000 --- a/java/com/android/dialer/speeddial/room/SpeedDialDatabaseComponent.java +++ /dev/null @@ -1,38 +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.speeddial.room; - -import android.content.Context; -import com.android.dialer.inject.HasRootComponent; -import dagger.Subcomponent; - -/** Dagger component for the speed dial room database. */ -@Subcomponent -public abstract class SpeedDialDatabaseComponent { - - public abstract SpeedDialEntryDatabase speedDialDatabase(); - - public static SpeedDialDatabaseComponent get(Context context) { - return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component()) - .speedDialDatabaseComponent(); - } - - /** Used to refer to the root application component. */ - public interface HasComponent { - SpeedDialDatabaseComponent speedDialDatabaseComponent(); - } -} diff --git a/java/com/android/dialer/speeddial/room/SpeedDialDatabaseModule.java b/java/com/android/dialer/speeddial/room/SpeedDialDatabaseModule.java deleted file mode 100644 index 8bb510087..000000000 --- a/java/com/android/dialer/speeddial/room/SpeedDialDatabaseModule.java +++ /dev/null @@ -1,35 +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.speeddial.room; - -import android.content.Context; -import com.android.dialer.inject.ApplicationContext; -import dagger.Module; -import dagger.Provides; -import javax.inject.Singleton; - -/** Dagger module which satisfies speed dial database dependencies. */ -@Module -public class SpeedDialDatabaseModule { - - @Provides - @Singleton - static SpeedDialEntryDatabase provideSpeedDialEntryDatabase( - @ApplicationContext Context appContext) { - return SpeedDialEntryDatabase.create(appContext); - } -} diff --git a/java/com/android/dialer/speeddial/room/SpeedDialEntry.java b/java/com/android/dialer/speeddial/room/SpeedDialEntry.java deleted file mode 100644 index 37d2d8b98..000000000 --- a/java/com/android/dialer/speeddial/room/SpeedDialEntry.java +++ /dev/null @@ -1,58 +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.speeddial.room; - -import android.arch.persistence.room.Entity; -import android.arch.persistence.room.PrimaryKey; -import android.support.annotation.IntDef; -import android.support.annotation.Nullable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** SpeedDialEntry Entity. Represents a single element in favorites. */ -@Entity -public class SpeedDialEntry { - - public static final int UNKNOWN = 0; - public static final int VOICE = 1; - public static final int VIDEO = 2; - - /** An Enum for the different row view types shown by this adapter. */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({UNKNOWN, VOICE, VIDEO}) - public @interface Type {} - - @PrimaryKey(autoGenerate = true) - public Integer id; - - @Nullable public final String number; - - public final long contactId; - - @Type public final int type; - - /** Build an unknown speed dial entry. */ - public static SpeedDialEntry newSpeedDialEntry(long contactId) { - return new SpeedDialEntry(null, contactId, UNKNOWN); - } - - public SpeedDialEntry(@Nullable String number, long contactId, @Type int type) { - this.number = number; - this.contactId = contactId; - this.type = type; - } -} diff --git a/java/com/android/dialer/speeddial/room/SpeedDialEntryDao.java b/java/com/android/dialer/speeddial/room/SpeedDialEntryDao.java deleted file mode 100644 index 3e440deab..000000000 --- a/java/com/android/dialer/speeddial/room/SpeedDialEntryDao.java +++ /dev/null @@ -1,44 +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.speeddial.room; - -import android.arch.persistence.room.Dao; -import android.arch.persistence.room.Delete; -import android.arch.persistence.room.Insert; -import android.arch.persistence.room.Query; -import android.arch.persistence.room.Update; -import java.util.List; - -/** Data access object for {@link SpeedDialEntry}. */ -@Dao -public interface SpeedDialEntryDao { - - @Query("SELECT * FROM speeddialentry") - List getAllEntries(); - - @Query("DELETE FROM speeddialentry") - void nukeTable(); - - @Insert - void insert(SpeedDialEntry... entries); - - @Update - void update(SpeedDialEntry... entries); - - @Delete - void delete(SpeedDialEntry... entries); -} diff --git a/java/com/android/dialer/speeddial/room/SpeedDialEntryDatabase.java b/java/com/android/dialer/speeddial/room/SpeedDialEntryDatabase.java deleted file mode 100644 index ff8e23743..000000000 --- a/java/com/android/dialer/speeddial/room/SpeedDialEntryDatabase.java +++ /dev/null @@ -1,55 +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.speeddial.room; - -import android.arch.persistence.room.Database; -import android.arch.persistence.room.Room; -import android.arch.persistence.room.RoomDatabase; -import android.content.Context; -import android.support.annotation.VisibleForTesting; -import javax.inject.Singleton; - -/** Database of {@link SpeedDialEntry}. */ -@Database( - entities = {SpeedDialEntry.class}, - // Version should not change unless SpeedDialEntry schema changes, then it should be incremented - version = 3 -) -@Singleton -public abstract class SpeedDialEntryDatabase extends RoomDatabase { - - private static final String DB_NAME = "speedDialEntryoDatabase.db"; - private static boolean allowMainThreadQueriesForTesting; - - /* package-private */ static SpeedDialEntryDatabase create(Context appContext) { - RoomDatabase.Builder builder = - Room.databaseBuilder(appContext, SpeedDialEntryDatabase.class, DB_NAME) - // TODO(calderwoodra): implement migration plan for database upgrades - .fallbackToDestructiveMigration(); - if (allowMainThreadQueriesForTesting) { - builder.allowMainThreadQueries(); - } - return builder.build(); - } - - public abstract SpeedDialEntryDao getSpeedDialEntryDao(); - - @VisibleForTesting - public static void allowMainThreadQueriesForTesting() { - allowMainThreadQueriesForTesting = true; - } -} -- cgit v1.2.3