diff options
Diffstat (limited to 'java/com/android/dialer/speeddial')
11 files changed, 317 insertions, 47 deletions
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<String> 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<RecyclerView.ViewHolde switch (cursor.getRowType(position)) { case RowType.HEADER: ((HeaderViewHolder) holder).setHeaderText(cursor.getHeader()); - ((HeaderViewHolder) holder).showAddButton(cursor.hasFavorites() && position == 0); + ((HeaderViewHolder) holder).showAddButton(position == 0); break; case RowType.STARRED: ((FavoritesViewHolder) holder).bind(context, cursor); diff --git a/java/com/android/dialer/speeddial/SpeedDialCursor.java b/java/com/android/dialer/speeddial/SpeedDialCursor.java index 1208daefd..d693f94db 100644 --- a/java/com/android/dialer/speeddial/SpeedDialCursor.java +++ b/java/com/android/dialer/speeddial/SpeedDialCursor.java @@ -22,14 +22,19 @@ import android.database.MatrixCursor; import android.database.MergeCursor; import android.support.annotation.IntDef; import android.support.annotation.StringRes; +import android.util.ArraySet; import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.speeddial.room.SpeedDialEntry; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.Set; /** Cursor for favorites contacts. */ -final class SpeedDialCursor extends MergeCursor { +public final class SpeedDialCursor extends MergeCursor { /** * Caps the speed dial list to contain at most 20 contacts, including favorites and suggestions. @@ -49,50 +54,55 @@ final class SpeedDialCursor extends MergeCursor { int SUGGESTION = 2; } - public static SpeedDialCursor newInstance(Cursor strequentCursor) { + public static SpeedDialCursor newInstance(Cursor strequentCursor, List<SpeedDialEntry> 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<SpeedDialEntry> 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<Long> 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<Cursor> 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<SpeedDialEntry> 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<SpeedDialEntry> 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<SpeedDialEntryDatabase> 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; + } +} |