From 62b96291dd2bf025f0f84ba0376301a31a6b8776 Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Mon, 9 Apr 2018 14:16:47 -0700 Subject: Implemented SpeedDialUiItemLoader. SpeedDialUiItemLoader builds a listenable future for returning a list of SpeedDialUiItems which are the POJO representation of each speed dial list element. Bug: 36841782 Test: SpeedDialContentObserverTest PiperOrigin-RevId: 192186376 Change-Id: I70f3abbeac14117ff4a68355e3a07b395b72386b --- .../dialer/speeddial/SpeedDialUiItemLoader.java | 225 +++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java (limited to 'java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java') diff --git a/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java b/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java new file mode 100644 index 000000000..257c74f58 --- /dev/null +++ b/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2018 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; + +import android.annotation.TargetApi; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.support.annotation.WorkerThread; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; +import com.android.dialer.inject.ApplicationContext; +import com.android.dialer.speeddial.database.SpeedDialEntry; +import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; +import com.android.dialer.speeddial.database.SpeedDialEntryDao; +import com.android.dialer.speeddial.database.SpeedDialEntryDatabaseHelper; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; + +/** + * Loads a list of {@link SpeedDialUiItem SpeedDialUiItems}. + * + * @see #loadSpeedDialUiItems() + *
    + *
  1. Retrieve the list of {@link SpeedDialEntry} from {@link SpeedDialEntryDatabaseHelper}. + *
  2. Build a list of {@link SpeedDialUiItem} based on {@link SpeedDialEntry#lookupKey()} in + * {@link Phone#CONTENT_URI}. + *
  3. Remove any {@link SpeedDialEntry} that is no longer starred or whose contact was + * deleted. + *
  4. Update each {@link SpeedDialEntry} contact id, lookup key and channel. + *
  5. Build a list of {@link SpeedDialUiItem} from {@link Contacts#STREQUENT_PHONE_ONLY}. + *
  6. If any starred contacts in that list aren't in the {@link + * SpeedDialEntryDatabaseHelper}, insert them now. + *
  7. Notify the {@link SuccessListener} of the complete list of {@link SpeedDialUiItem + * SpeedDialContacts} composed from {@link SpeedDialEntry SpeedDialEntries} and + * non-starred {@link Contacts#STREQUENT_PHONE_ONLY}. + *
+ */ +@SuppressWarnings("AndroidApiChecker") +@TargetApi(VERSION_CODES.N) +public final class SpeedDialUiItemLoader { + + private final Context appContext; + private final ListeningExecutorService backgroundExecutor; + + @Inject + public SpeedDialUiItemLoader( + @ApplicationContext Context appContext, + @BackgroundExecutor ListeningExecutorService backgroundExecutor) { + this.appContext = appContext; + this.backgroundExecutor = backgroundExecutor; + } + + /** + * Returns a {@link ListenableFuture} for a list of {@link SpeedDialUiItem SpeedDialUiItems}. This + * list is composed of starred contacts from {@link SpeedDialEntryDatabaseHelper} and suggestions + * from {@link Contacts#STREQUENT_PHONE_ONLY}. + */ + public ListenableFuture> loadSpeedDialUiItems() { + return backgroundExecutor.submit(this::doInBackground); + } + + @WorkerThread + private ImmutableList doInBackground() { + Assert.isWorkerThread(); + SpeedDialEntryDao db = new SpeedDialEntryDatabaseHelper(appContext); + + // This is the list of contacts that we will display to the user + List speedDialUiItems = new ArrayList<>(); + + // We'll use these lists to update the SpeedDialEntry database + List entriesToInsert = new ArrayList<>(); + List entriesToUpdate = new ArrayList<>(); + List entriesToDelete = new ArrayList<>(); + + // Track the highest entry ID + // TODO(a bug): use auto-generated IDs + long maxId = 0L; + + // Get all SpeedDialEntries and mark them to be updated or deleted + List entries = db.getAllEntries(); + for (SpeedDialEntry entry : entries) { + maxId = Math.max(entry.id(), maxId); + + SpeedDialUiItem contact = getSpeedDialContact(entry); + // Remove contacts that no longer exist or are no longer starred + if (contact == null || !contact.isStarred()) { + entriesToDelete.add(entry.id()); + continue; + } + + // Contact exists, so update its entry in SpeedDialEntry Database + entriesToUpdate.add( + entry + .toBuilder() + .setLookupKey(contact.lookupKey()) + .setContactId(contact.contactId()) + .setDefaultChannel(contact.defaultChannel()) + .build()); + + // These are our existing starred entries + speedDialUiItems.add(contact); + } + + // Get all Strequent Contacts + List strequentContacts = getStrequentContacts(); + + // For each contact, if it isn't starred, add it as a suggestion. + // If it is starred and not already accounted for above, then insert into the SpeedDialEntry DB. + for (SpeedDialUiItem contact : strequentContacts) { + if (!contact.isStarred()) { + // Add this contact as a suggestion + // TODO(calderwoodra): set the defaults of these automatically + speedDialUiItems.add(contact); + + } else if (speedDialUiItems.stream().noneMatch(c -> c.contactId() == contact.contactId())) { + // Increment the ID so there aren't any collisions + maxId += 1; + entriesToInsert.add( + SpeedDialEntry.builder() + .setId(maxId) + .setLookupKey(contact.lookupKey()) + .setContactId(contact.contactId()) + .setDefaultChannel(contact.defaultChannel()) + .build()); + + // These are our newly starred contacts + speedDialUiItems.add(contact); + } + } + + // TODO(a bug): use a single db transaction + db.delete(entriesToDelete); + db.update(entriesToUpdate); + db.insert(entriesToInsert); + return ImmutableList.copyOf(speedDialUiItems); + } + + @WorkerThread + private SpeedDialUiItem getSpeedDialContact(SpeedDialEntry entry) { + Assert.isWorkerThread(); + // TODO(b77725860): Might need to use the lookup uri to get the contact id first, then query + // based on that. + SpeedDialUiItem contact; + try (Cursor cursor = + appContext + .getContentResolver() + .query( + Phone.CONTENT_URI, + SpeedDialUiItem.PHONE_PROJECTION, + Phone.NUMBER + " IS NOT NULL AND " + Phone.LOOKUP_KEY + "=?", + new String[] {entry.lookupKey()}, + null)) { + + if (cursor == null || cursor.getCount() == 0) { + // Contact not found, potentially deleted + LogUtil.e("SpeedDialUiItemLoader.getSpeedDialContact", "Contact not found."); + return null; + } + + cursor.moveToFirst(); + contact = SpeedDialUiItem.fromCursor(cursor); + } + + // Preserve the default channel if it didn't change/still exists + Channel defaultChannel = entry.defaultChannel(); + if (defaultChannel != null) { + if (contact.channels().contains(defaultChannel)) { + contact = contact.toBuilder().setDefaultChannel(defaultChannel).build(); + } + } + + // TODO(calderwoodra): Consider setting the default channel if there is only one channel + return contact; + } + + @WorkerThread + private List getStrequentContacts() { + Assert.isWorkerThread(); + Uri uri = + Contacts.CONTENT_STREQUENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true") + .build(); + try (Cursor cursor = + appContext + .getContentResolver() + .query(uri, SpeedDialUiItem.PHONE_PROJECTION, null, null, null)) { + List contacts = new ArrayList<>(); + if (cursor == null || cursor.getCount() == 0) { + return contacts; + } + + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + contacts.add(SpeedDialUiItem.fromCursor(cursor)); + } + return contacts; + } + } +} -- cgit v1.2.3 From 178b452d6ade018ebb7f2209ccc8233d4fa8acaa Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Mon, 9 Apr 2018 14:47:58 -0700 Subject: Autogenerate IDs for inserts and consolidate loader into one transaction. Bug: 36841782,77724716,77725859 Test: implemented PiperOrigin-RevId: 192191296 Change-Id: I7a22367b33c7555d014a29a2af2942f2eb76c0a5 --- .../dialer/speeddial/SpeedDialUiItemLoader.java | 17 +-- .../dialer/speeddial/database/SpeedDialEntry.java | 11 +- .../speeddial/database/SpeedDialEntryDao.java | 26 +++-- .../database/SpeedDialEntryDatabaseHelper.java | 125 +++++++++++++++------ 4 files changed, 123 insertions(+), 56 deletions(-) (limited to 'java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java') diff --git a/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java b/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java index 257c74f58..13e5f8744 100644 --- a/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java +++ b/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java @@ -97,15 +97,9 @@ public final class SpeedDialUiItemLoader { List entriesToUpdate = new ArrayList<>(); List entriesToDelete = new ArrayList<>(); - // Track the highest entry ID - // TODO(a bug): use auto-generated IDs - long maxId = 0L; - // Get all SpeedDialEntries and mark them to be updated or deleted List entries = db.getAllEntries(); for (SpeedDialEntry entry : entries) { - maxId = Math.max(entry.id(), maxId); - SpeedDialUiItem contact = getSpeedDialContact(entry); // Remove contacts that no longer exist or are no longer starred if (contact == null || !contact.isStarred()) { @@ -138,11 +132,8 @@ public final class SpeedDialUiItemLoader { speedDialUiItems.add(contact); } else if (speedDialUiItems.stream().noneMatch(c -> c.contactId() == contact.contactId())) { - // Increment the ID so there aren't any collisions - maxId += 1; entriesToInsert.add( SpeedDialEntry.builder() - .setId(maxId) .setLookupKey(contact.lookupKey()) .setContactId(contact.contactId()) .setDefaultChannel(contact.defaultChannel()) @@ -153,10 +144,10 @@ public final class SpeedDialUiItemLoader { } } - // TODO(a bug): use a single db transaction - db.delete(entriesToDelete); - db.update(entriesToUpdate); - db.insert(entriesToInsert); + db.insertUpdateAndDelete( + ImmutableList.copyOf(entriesToInsert), + ImmutableList.copyOf(entriesToUpdate), + ImmutableList.copyOf(entriesToDelete)); return ImmutableList.copyOf(speedDialUiItems); } diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java index f63619480..5b54b79c8 100644 --- a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java +++ b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java @@ -27,8 +27,13 @@ import java.lang.annotation.RetentionPolicy; @AutoValue public abstract class SpeedDialEntry { - /** Unique ID */ - public abstract long id(); + /** + * Unique ID + * + *

Must be null when inserting, and an ID will be generated and returned after inserting. + */ + @Nullable + public abstract Long id(); /** @see {@link Contacts#_ID} */ public abstract long contactId(); @@ -55,7 +60,7 @@ public abstract class SpeedDialEntry { @AutoValue.Builder public abstract static class Builder { - public abstract Builder setId(long id); + public abstract Builder setId(Long id); public abstract Builder setContactId(long contactId); diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java index 0efc110f4..ce771c3c8 100644 --- a/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java +++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java @@ -16,7 +16,7 @@ package com.android.dialer.speeddial.database; -import java.util.List; +import com.google.common.collect.ImmutableList; /** * Interface that databases support speed dial entries should implement. @@ -26,19 +26,19 @@ import java.util.List; public interface SpeedDialEntryDao { /** Return all entries in the database */ - List getAllEntries(); + ImmutableList getAllEntries(); /** * Insert new entries. * - *

Fails if any of the {@link SpeedDialEntry#id()} already exist. + *

{@link SpeedDialEntry#id() ids} must be null. */ - void insert(List entries); + void insert(ImmutableList entries); /** * Insert a new entry. * - *

Fails if the {@link SpeedDialEntry#id()} already exists. + *

{@link SpeedDialEntry#id() ids} must be null. */ long insert(SpeedDialEntry entry); @@ -47,14 +47,26 @@ public interface SpeedDialEntryDao { * *

Fails if the {@link SpeedDialEntry#id()} doesn't exist. */ - void update(List entries); + void update(ImmutableList entries); /** * Delete the passed in entries based on {@link SpeedDialEntry#id}. * *

Fails if the {@link SpeedDialEntry#id()} doesn't exist. */ - void delete(List entries); + void delete(ImmutableList entries); + + /** + * Inserts, updates and deletes rows all in on transaction. + * + * @see #insert(ImmutableList) + * @see #update(ImmutableList) + * @see #delete(ImmutableList) + */ + void insertUpdateAndDelete( + ImmutableList entriesToInsert, + ImmutableList entriesToUpdate, + ImmutableList entriesToDelete); /** Delete all entries in the database. */ void deleteAll(); diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java index 01d49c3d7..7c823bd63 100644 --- a/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java +++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java @@ -25,6 +25,7 @@ import android.text.TextUtils; import com.android.dialer.common.Assert; import com.android.dialer.common.database.Selection; import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; @@ -94,7 +95,7 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper } @Override - public List getAllEntries() { + public ImmutableList getAllEntries() { List entries = new ArrayList<>(); String query = "SELECT * FROM " + TABLE_NAME; @@ -118,25 +119,24 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper .setDefaultChannel(channel) .setContactId(cursor.getLong(POSITION_CONTACT_ID)) .setLookupKey(cursor.getString(POSITION_LOOKUP_KEY)) - .setId(cursor.getInt(POSITION_ID)) + .setId(cursor.getLong(POSITION_ID)) .build(); entries.add(entry); } } - return entries; + return ImmutableList.copyOf(entries); } @Override - public void insert(List entries) { + public void insert(ImmutableList entries) { + if (entries.isEmpty()) { + return; + } + SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { - for (SpeedDialEntry entry : entries) { - if (db.insert(TABLE_NAME, null, buildContentValues(entry)) == -1L) { - throw Assert.createUnsupportedOperationFailException( - "Attempted to insert a row that already exists."); - } - } + insert(db, entries); db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -144,11 +144,21 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper } } + private void insert(SQLiteDatabase writeableDatabase, ImmutableList entries) { + for (SpeedDialEntry entry : entries) { + Assert.checkArgument(entry.id() == null); + if (writeableDatabase.insert(TABLE_NAME, null, buildContentValuesWithoutId(entry)) == -1L) { + throw Assert.createUnsupportedOperationFailException( + "Attempted to insert a row that already exists."); + } + } + } + @Override public long insert(SpeedDialEntry entry) { long updateRowId; try (SQLiteDatabase db = getWritableDatabase()) { - updateRowId = db.insert(TABLE_NAME, null, buildContentValues(entry)); + updateRowId = db.insert(TABLE_NAME, null, buildContentValuesWithoutId(entry)); } if (updateRowId == -1) { throw Assert.createUnsupportedOperationFailException( @@ -158,22 +168,15 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper } @Override - public void update(List entries) { + public void update(ImmutableList entries) { + if (entries.isEmpty()) { + return; + } + SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { - for (SpeedDialEntry entry : entries) { - int count = - db.update( - TABLE_NAME, - buildContentValues(entry), - ID + " = ?", - new String[] {Long.toString(entry.id())}); - if (count != 1) { - throw Assert.createUnsupportedOperationFailException( - "Attempted to update an undetermined number of rows: " + count); - } - } + update(db, entries); db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -181,9 +184,34 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper } } - private ContentValues buildContentValues(SpeedDialEntry entry) { + private void update(SQLiteDatabase writeableDatabase, ImmutableList entries) { + for (SpeedDialEntry entry : entries) { + int count = + writeableDatabase.update( + TABLE_NAME, + buildContentValuesWithId(entry), + ID + " = ?", + new String[] {Long.toString(entry.id())}); + if (count != 1) { + throw Assert.createUnsupportedOperationFailException( + "Attempted to update an undetermined number of rows: " + count); + } + } + } + + private ContentValues buildContentValuesWithId(SpeedDialEntry entry) { + return buildContentValues(entry, true); + } + + private ContentValues buildContentValuesWithoutId(SpeedDialEntry entry) { + return buildContentValues(entry, false); + } + + private ContentValues buildContentValues(SpeedDialEntry entry, boolean includeId) { ContentValues values = new ContentValues(); - values.put(ID, entry.id()); + if (includeId) { + values.put(ID, entry.id()); + } values.put(CONTACT_ID, entry.contactId()); values.put(LOOKUP_KEY, entry.lookupKey()); if (entry.defaultChannel() != null) { @@ -195,19 +223,50 @@ public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper } @Override - public void delete(List ids) { + public void delete(ImmutableList ids) { + if (ids.isEmpty()) { + return; + } + + try (SQLiteDatabase db = getWritableDatabase()) { + delete(db, ids); + } + } + + private void delete(SQLiteDatabase writeableDatabase, ImmutableList ids) { List idStrings = new ArrayList<>(); for (Long id : ids) { idStrings.add(Long.toString(id)); } Selection selection = Selection.builder().and(Selection.column(ID).in(idStrings)).build(); - try (SQLiteDatabase db = getWritableDatabase()) { - int count = db.delete(TABLE_NAME, selection.getSelection(), selection.getSelectionArgs()); - if (count != ids.size()) { - throw Assert.createUnsupportedOperationFailException( - "Attempted to delete an undetermined number of rows: " + count); - } + int count = + writeableDatabase.delete( + TABLE_NAME, selection.getSelection(), selection.getSelectionArgs()); + if (count != ids.size()) { + throw Assert.createUnsupportedOperationFailException( + "Attempted to delete an undetermined number of rows: " + count); + } + } + + @Override + public void insertUpdateAndDelete( + ImmutableList entriesToInsert, + ImmutableList entriesToUpdate, + ImmutableList entriesToDelete) { + if (entriesToInsert.isEmpty() && entriesToUpdate.isEmpty() && entriesToDelete.isEmpty()) { + return; + } + SQLiteDatabase db = getWritableDatabase(); + db.beginTransaction(); + try { + insert(db, entriesToInsert); + update(db, entriesToUpdate); + delete(db, entriesToDelete); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + db.close(); } } -- cgit v1.2.3