summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java
blob: 257c74f58f6e5a34ec878fd6cc2720588ea07863 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
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()
 *     <ol>
 *       <li>Retrieve the list of {@link SpeedDialEntry} from {@link SpeedDialEntryDatabaseHelper}.
 *       <li>Build a list of {@link SpeedDialUiItem} based on {@link SpeedDialEntry#lookupKey()} in
 *           {@link Phone#CONTENT_URI}.
 *       <li>Remove any {@link SpeedDialEntry} that is no longer starred or whose contact was
 *           deleted.
 *       <li>Update each {@link SpeedDialEntry} contact id, lookup key and channel.
 *       <li>Build a list of {@link SpeedDialUiItem} from {@link Contacts#STREQUENT_PHONE_ONLY}.
 *       <li>If any starred contacts in that list aren't in the {@link
 *           SpeedDialEntryDatabaseHelper}, insert them now.
 *       <li>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}.
 *     </ol>
 */
@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<ImmutableList<SpeedDialUiItem>> loadSpeedDialUiItems() {
    return backgroundExecutor.submit(this::doInBackground);
  }

  @WorkerThread
  private ImmutableList<SpeedDialUiItem> doInBackground() {
    Assert.isWorkerThread();
    SpeedDialEntryDao db = new SpeedDialEntryDatabaseHelper(appContext);

    // This is the list of contacts that we will display to the user
    List<SpeedDialUiItem> speedDialUiItems = new ArrayList<>();

    // We'll use these lists to update the SpeedDialEntry database
    List<SpeedDialEntry> entriesToInsert = new ArrayList<>();
    List<SpeedDialEntry> entriesToUpdate = new ArrayList<>();
    List<Long> 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<SpeedDialEntry> 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<SpeedDialUiItem> 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<SpeedDialUiItem> 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<SpeedDialUiItem> contacts = new ArrayList<>();
      if (cursor == null || cursor.getCount() == 0) {
        return contacts;
      }

      cursor.moveToPosition(-1);
      while (cursor.moveToNext()) {
        contacts.add(SpeedDialUiItem.fromCursor(cursor));
      }
      return contacts;
    }
  }
}